Claude‑Code CLI Wrapper (Free)

A minimal Python CLI using free‑claude‑code

Welcome to another official UVF IT guide! In this quick build you’ll create a tiny command‑line wrapper around the free‑claude‑code library, letting you send a prompt and get back generated code – all without any paid API keys.

We’ll use plain Python (3.12+), the free‑claude‑code package from PyPI, and a local SQLite cache for history. In ~45 minutes you’ll have a working claude-cli you can extend with your own agents or UI.

Before you start

Minimal Claude‑Code CLI Architecture
LOCAL RUNTIME calls stores CLI Wrapper (Python) free‑claude‑code lib SQLite history DB
1

Phase 1: Scaffold the project

First we create an isolated workspace so our dependencies don’t clash with other Python projects. A virtual environment also makes the final CLI portable – you can zip the folder and run it on any machine with the same Python version.

Next we pull in the free-claude-code library from PyPI. It contains the thin wrapper around Claude’s free endpoint, which we’ll call from our own script.

mkdir claude-cli && cd claude-cli
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install free-claude-code
2

Phase 2: Define a tiny data model

We want to remember each prompt and the code Claude returns, so we’ll store them in a local SQLite file. The built‑in sqlite3 module is perfect – no external server, just a single file next to our script.

The helper below creates the database on first run, provides add_entry(prompt, response) and list_history() utilities, and hides all raw SQL behind simple Python functions.

history.py

import sqlite3
from pathlib import Path

DB_PATH = Path(__file__).with_name('history.db')

def _init_db() -> sqlite3.Connection:
    """Create the DB file and the history table if they do not exist."""
    conn = sqlite3.connect(DB_PATH)
    conn.execute(
        """CREATE TABLE IF NOT EXISTS history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            prompt TEXT NOT NULL,
            response TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )"""
    )
    conn.commit()
    return conn

_CONN = _init_db()

def add_entry(prompt: str, response: str) -> None:
    """Insert a new prompt/response pair into the DB."""
    _CONN.execute(
        "INSERT INTO history (prompt, response) VALUES (?, ?)",
        (prompt, response),
    )
    _CONN.commit()

def list_history(limit: int = 20) -> list[tuple[int, str, str, str]]:
    """Return the most recent *limit* rows as a list of tuples.
    Each tuple is (id, prompt, response, created_at)."""
    cur = _CONN.execute(
        "SELECT id, prompt, response, created_at FROM history ORDER BY id DESC LIMIT ?",
        (limit,),
    )
    return cur.fetchall()
3

Phase 3: Build the CLI core

In this step we wire everything together. The script will read a prompt from the command line, ask the Claude‑Code client for a completion, store the request and response in the SQLite log, and finally print the generated code along with a short history snippet. Keeping the logic in a single cli.py file makes it easy to extend later, for example by adding flags for different agents or output formats.

We also add a tiny helper that fetches the last few entries from the history.db file so users can see what they asked before. This mirrors the behaviour of many AI‑powered CLIs and gives immediate feedback that the persistence layer works.

cli.py

#!/usr/bin/env python3
import sys, json, sqlite3
from pathlib import Path

# Import the free‑claude‑code client that was scaffolded in earlier phases
from client import ClaudeClient  # assumes client.py provides ClaudeClient

DB_PATH = Path('history.db')

def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute('''CREATE TABLE IF NOT EXISTS logs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        prompt TEXT NOT NULL,
        response TEXT NOT NULL,
        timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
    )''')
    conn.commit()
    return conn

def log_interaction(conn, prompt, response):
    conn.execute('INSERT INTO logs (prompt, response) VALUES (?, ?)', (prompt, response))
    conn.commit()

def recent_history(conn, limit=3):
    cur = conn.execute('SELECT prompt FROM logs ORDER BY id DESC LIMIT ?', (limit,))
    return [row[0] for row in cur.fetchall()][::-1]

def main():
    if len(sys.argv) < 2:
        print('Usage: cli.py "<your prompt>"')
        sys.exit(1)
    prompt = sys.argv[1]
    client = ClaudeClient()
    response = client.generate_code(prompt)  # returns a string of code
    conn = init_db()
    log_interaction(conn, prompt, response)
    print('\n--- Generated Code ---\n')
    print(response)
    print('\n--- Recent History ---')
    for p in recent_history(conn):
        print(f'- {p}')

if __name__ == '__main__':
    main()
4

Phase 4: Run locally

Now that the CLI core is ready, we make the script executable and give it a quick test run. The chmod +x command marks cli.py as runnable, and invoking it with a simple prompt should produce generated code and a short history dump. If everything is wired correctly you’ll also see a new history.db file appear in the project root.

Running the script directly from the terminal mirrors how end‑users will interact with the tool, so any missing imports or runtime errors will surface immediately.

chmod +x cli.py
./cli.py "Create a hello world program in Rust"
5

Phase 5: Verify and extend

After the first run, verify that the CLI printed the expected Rust snippet and that a SQLite database was created. You can also query the history.db file to ensure the prompt was logged; this confirms both the client call and the persistence layer are functional. From here you might add command‑line flags like --agent to switch providers or spin up a tiny FastAPI wrapper to expose the same functionality over HTTP.

# Check that the DB exists and has at least one row
python - <<'PY'
import sqlite3, os
assert os.path.exists('history.db'), 'history.db missing'
conn = sqlite3.connect('history.db')
count = conn.execute('SELECT COUNT(*) FROM logs').fetchone()[0]
print('Rows in logs:', count)
PY

# Show the last stored prompt
python - <<'PY'
import sqlite3
conn = sqlite3.connect('history.db')
rows = conn.execute('SELECT prompt FROM logs ORDER BY id DESC LIMIT 1').fetchall()
print('Last prompt:', rows[0][0] if rows else 'none')
PY

Verifying the Stack

CheckCommand / Why it matters
CLI returns generated code for a prompt./cli.py "Create a hello world in Rust"
history.db file exists and contains at least one rowpython -c "import sqlite3,os;assert os.path.exists('history.db');print(sqlite3.connect('history.db').execute('SELECT count(*) FROM logs').fetchone())"
Recent history prints after the responseObserve the "--- Recent history ---" section in the CLI output

What’s next?

You now have a functional Claude‑Code wrapper. Experiment by adding --agent flags to select different providers, or spin up a tiny FastAPI endpoint to expose the CLI over HTTP.