A local, terminal-based AI coding agent for Linux, WSL, and Termux. PyClaw connects to a local or self-hosted OpenAI-compatible model server and lets it read, search, and edit a real project on disk through a defined set of tools, with every destructive action gated behind explicit approval.
Status: Early development. Core functionality is implemented, but APIs and features may change as the project evolves.
This README is written as a complete, beginner-friendly tutorial. If you've never used PyClaw (or a tool like it) before, read it top to bottom; if you just need a specific command, jump to Complete command reference.
- What PyClaw actually does
- Requirements
- Step-by-step setup
- First launch
- Getting started: your first few minutes
- Complete command reference
- Keyboard shortcuts
- Example commands with expected output
- The agent toggle: full agent vs. direct-response mode
- How intent classification works
- Skills
- Project Instructions (PYCLAW.md)
- Backends and switching models
- Safety model
- Project layout
- Troubleshooting
PyClaw is a program that sits between you and a language model, and gives that model hands: the ability to read files, search a codebase, edit files (with your approval), run shell commands, and use git -- inside one project folder on your own machine.
It is not a chatbot that only talks about code. When you ask it to fix a bug, it actually opens the relevant file, reads it, proposes a specific change as a diff, and only writes that change after you approve it. When you just say "hi," it answers like a person would -- no file scanning, no multi-step planning, nothing happening behind the scenes.
The core pieces:
- An LLM server you run yourself (llama.cpp, Ollama, LM Studio, or a hosted API) -- PyClaw does not include or run a model itself.
- PyClaw, the program in this repository -- the interface, the safety rules, and the tools (read/write/search/run/git) the model is allowed to use.
- You, approving every edit and every risky action before it happens.
Everything PyClaw does to your files is sandboxed to one project
directory at a time, and nothing destructive happens without you typing
y.
- Python 3.11+
git(for installation and the git tools)- A model server implementing the OpenAI-compatible chat completions API:
llama.cpp's
llama-server, Ollama, LM Studio, vLLM, text-generation-webui, or a hosted equivalent.
PyClaw does not bundle a model or a model server -- you provide the backend, PyClaw provides the agent and interface around it.
git clone https://github.com/MaxEdgar/pyclaw.git
cd pyclaw(Or unzip a downloaded copy and cd into that folder.)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtOr use the installer script, which does the same three steps for you:
chmod +x install.sh
./install.shPyClaw needs something to talk to. The example below uses llama.cpp with a small local model; any OpenAI-compatible server works the same way.
./llama-server -m /path/to/your-model.gguf \
--host 127.0.0.1 --port 8080 --ctx-size 8192Leave this running in its own terminal/tab -- don't close it. In a second terminal, you can sanity-check it's alive with:
curl http://127.0.0.1:8080/healthIf that returns a response (not "connection refused"), the server is up.
source .venv/bin/activate # if not already active
python main.pyThat's the entire setup. No database, no extra services, no account creation.
The first time PyClaw runs, it creates three files under ~/.pyclaw/:
config.json-- your settings (model server address, theme, toggles)session.json-- your current task, recent files, todo listhistory.jsonl-- the full conversation transcript
You'll see a brief startup animation (the full Textual UI only; the
--no-tui REPL shows a plain text banner instead), then the main
interface with a chat box at the bottom. If no model server is reachable
yet, the status bar will say Offline -- that's expected and not an
error; start your server and PyClaw will detect it automatically within
about 15 seconds, or immediately if you send a message.
On Termux, PyClaw defaults to the simpler --no-tui mode automatically
(a known Android soft-keyboard quirk with the full interface -- see
Troubleshooting). Pass --tui to force the full
interface anyway.
A few small things to try right away, in order:
1. Say hello.
> hi
PyClaw answers directly and instantly -- no scanning, no plan, no tool calls. This is deliberate: greetings and small talk never trigger the agent system (see How intent classification works).
2. Ask it to look at your project.
> summarize this project
Now it actually reads your project structure and answers based on what's really there.
3. Ask it to explain a specific file.
> what does main.py do?
4. Try something that needs an edit.
> add a docstring to the first function in utils.py
PyClaw will read the file, propose the change as a colored diff, and ask
Approve patch? [Y] Yes [N] No before touching anything.
5. Look at what it remembers.
/memory
/history
That's the whole loop: ask, it investigates if needed, it shows you exactly what it wants to change, you approve or decline.
Every command below works in both the full Textual UI and the --no-tui
REPL unless noted otherwise.
| Command | What it does | When to use it |
|---|---|---|
/help |
Shows the command reference | You forgot a command |
/clear |
Clears the conversation and session memory (todos are kept -- see /todo clear) |
Starting a fresh topic in the same project |
/history |
Shows recent chat history | Reviewing what was discussed earlier |
/project |
Shows the current project root | Confirming which folder PyClaw is operating on |
/project <path> |
Switches the active project root | Working on a different project without restarting |
/model |
Shows a numbered list of backend presets and saved aliases | Seeing what you can switch to |
/model <number> |
Switches to that numbered choice instantly | Quick backend switching, e.g. /model 2 |
/model <name> |
Switches to a preset or alias by name | e.g. /model ollama or /model primary |
/model <field> <value> |
Sets one field directly (base_url, model_name, temperature, context_size, max_tokens, top_p, api_key) |
Fine-tuning a setting without a full preset |
/model alias save <name> |
Saves the current backend+model under a short name | Creating a "primary"/"fast" shortcut |
/model alias use <name> |
Switches to a saved alias | Recalling a saved shortcut |
/model alias delete <name> |
Removes a saved alias | Cleaning up |
/tools |
Lists every tool the agent can use | Curiosity, or debugging what it's capable of |
/memory |
Shows the current session summary (task, recent files, todos) | Checking what PyClaw currently "remembers" |
/skill list |
Lists saved skills | Seeing what's taught |
/skill create [name] |
Starts a guided flow to teach a new skill | Teaching a reusable convention |
/skill show <name> |
Shows one skill's full details | Reviewing what a skill actually says |
/skill delete <name> |
Removes a skill | Cleaning up |
/theme list |
Lists color themes with their actual hex values | Picking a look |
/theme set <name> |
Switches theme | e.g. /theme set pink |
/doctor |
Audits your configuration and connectivity | Something feels wrong; a quick health check |
/todo add <text> |
Adds a persistent todo item | Tracking multi-step or multi-day work |
/todo list |
Shows the todo list | Checking what's left |
/todo done <number> |
Marks a todo item complete | Finishing an item |
/todo clear |
Removes all todo items | Starting a fresh task list |
/agent |
Shows whether the agent system is on or off | Checking current mode |
/agent on |
Enables full agent behavior (planning + tools) | Default mode -- back to normal |
/agent off |
Disables agent behavior entirely; direct-response only | You want a plain Q&A chat with no file/tool access at all |
/quit, /exit |
Exits PyClaw | Done for now |
Typing / alone (in the full UI) shows a live, filtered list of matching
commands as you type.
These work in the full Textual UI (python main.py, not --no-tui).
| Key | Action |
|---|---|
Ctrl+X |
Cancel the current request, or decline a pending confirmation |
Ctrl+L |
Clear the conversation (same as /clear) |
Ctrl+C |
Quit |
Ctrl+P |
Open Textual's built-in command palette (change theme, save a screenshot, etc.) |
Up / Down |
Recall previously submitted input (shell-style history) |
Tab |
Complete the highlighted slash-command suggestion |
Asking a simple question (chat mode, instant, no tools):
> hey
Agent: Hey! What are you working on?
Asking about the project (task mode, reads real files):
> what does the search tool do?
-> grep_text(query="def search_files")
Found 1 match(es) for 'def search_files'
-> read_file(path="tools/search.py")
Read 1842 bytes from tools/search.py
Agent: search_files() finds files by glob pattern (like "*.py"), matching
against filenames recursively from the project root, skipping directories
like .git and node_modules.
Asking for an edit (full plan + diff approval):
> fix the off-by-one error in paginate()
Agent Plan
1. Search the project for paginate(
2. Read the file it's defined in
3. Identify the bug
4. Propose a fix
-> grep_text(query="def paginate")
-> read_file(path="utils/pagination.py")
-> propose_patch(path="utils/pagination.py", new_content="...")
Patch: utils/pagination.py (+1 / -1)
--- a/utils/pagination.py
+++ b/utils/pagination.py
@@ -12,7 +12,7 @@
- return items[start:end]
+ return items[start:end + 1]
Approve patch? [Y] Yes [N] No
> y
Agent: Fixed the off-by-one in paginate() -- end was exclusive when it
should have included the last item on the page. Patch applied.
Switching models:
> /model
Current: auto @ http://127.0.0.1:8080
Choose with /model <number> or /model <name>:
1. llamacpp [preset] -- auto @ http://127.0.0.1:8080
2. ollama [preset] -- auto @ http://127.0.0.1:11434
3. lmstudio [preset] -- auto @ http://127.0.0.1:1234
4. openai [preset] -- gpt-4o-mini @ https://api.openai.com
> /model 2
Switched to ollama (auto @ http://127.0.0.1:11434) -- checking connection...
ollama: connected.
Turning the agent off for a plain-chat session:
> /agent off
Agent system disabled. PyClaw will respond directly to every message --
no planning, no tool execution, no file/codebase access. Use /agent on to
re-enable.
> can you refactor my auth module
Agent: I can't make changes right now -- agent mode is off. Turn it back
on with /agent on if you'd like me to look at your code.
/agent off is a hard switch, separate from how individual messages are
classified (see the next section). With it off:
- No planning. The planning step never runs, for any message.
- No tool execution. PyClaw cannot read files, search the project, run commands, or edit anything -- even if you explicitly ask it to.
- No multi-step reasoning. Every message gets exactly one direct reply, the same as a plain chat with the model.
This is stronger than intent classification skipping planning for a
greeting: with the agent off, even an explicit "fix this bug, refactor
the whole module" request gets a plain conversational reply instead of
any real action. Turn it back on with /agent on whenever you want full
capability again. The setting persists across restarts until you change
it.
Before anything else happens, every message you send is classified into one of four intents:
- CHAT_INTENT -- greetings, small talk, short acknowledgements ("hi", "hello", "hey", "yo", "sup", "how are you", "thanks"). Answered directly and instantly. No planning, no tools, no filesystem access, no "Agent Plan" output -- ever, for this intent.
- TASK_INTENT -- an explicit request to do something ("create a folder", "fix this bug", "build the project"). Full agent behavior is allowed: planning, reading files, running tools, editing code.
- TOOL_REQUEST_INTENT -- a specific, named action ("run pytest", "show me the git diff"). Tools are allowed; the planning step is skipped since a single named action rarely needs a multi-step plan.
- SYSTEM_INTENT -- a slash command. These are handled before reaching the agent at all.
Classification is a fast, local check -- not an extra call to the model -- so it costs nothing and adds no delay. A short message (three words or fewer) with no recognizable action verb defaults to CHAT_INTENT, which is what stops something like "hi" or "yo" from accidentally being treated as a multi-step task. This is also what fixed a real bug where simple greetings were triggering project scanning and planning loops.
You can't disable intent classification itself (it's what keeps casual
messages fast and side-effect-free), but you can disable agent behavior
entirely for every message with /agent off, described above.
Skills are reusable instructions you teach PyClaw once, stored as JSON
under ~/.pyclaw/skills/, and automatically surfaced when relevant to a
request (matched by keyword overlap, not by always loading every skill
into every conversation). Full documentation, including the file format
and guidance on writing effective skills, is in
docs/SKILLS.md.
Two ready-to-use examples ship in examples/skills/ --
copy them into ~/.pyclaw/skills/ to try the feature immediately instead
of writing one from scratch first:
cp examples/skills/*.json ~/.pyclaw/skills/If a project contains a PYCLAW.md file at its root, PyClaw reads it
automatically on every task-intent request for that project -- no command
needed. Use it for conventions everyone working in the project should
follow (code style, directories to never touch, which test runner to
use):
# Project conventions
- Use 4-space indentation, never tabs.
- Never modify anything under vendor/.
- Run `pytest` for tests, not `unittest`.This is distinct from a skill: PYCLAW.md is scoped to the project and
applies to every task-intent request, like a README meant to be committed
to the repo. A skill is scoped to you and only activates when its
trigger keywords match what you typed.
PyClaw works with any server implementing the OpenAI-compatible
/v1/chat/completions endpoint -- not just one model or one vendor.
/model # see a numbered list of presets + your saved aliases
/model 2 # switch to choice #2
/model ollama # or switch by preset/alias name directlySave your own shortcuts:
/model alias save primary # snapshot the currently active backend+model
/model alias save fast # ...and a different one
/model primary # switch back with one word, any timePyClaw queries the server's /v1/models endpoint on connect to discover
which model is actually loaded, so model_name does not need to be set
by hand for local single-model servers.
- All filesystem tools are sandboxed to the configured project root; a path that resolves outside it is rejected before anything is read or written.
- Overwriting an existing file, deleting a file or directory, and any shell command matching a destructive pattern require interactive approval -- a Y/N modal in the Textual UI, a blocking prompt in the REPL.
- File edits are always presented as a diff before being applied.
- Cancelling with
Ctrl+Xwhile a confirmation is pending is treated as a decline, not left hanging. - No planning, tool execution, or agent loop can begin for a message
classified as CHAT_INTENT, and none of it can run at all while
/agent offis active -- both are enforced before the request ever reaches the model with tool information in its context. - A skill or
PYCLAW.mdfile is plain text read by the model as additional context. Neither can execute code or bypass the approval flow above.
pyclaw/
├── main.py # CLI entry point, simple REPL fallback
├── config.py # JSON-backed configuration (model, agent, theme settings)
├── install.sh # Linux installation script
├── llm/
│ ├── client.py # OpenAI-compatible HTTP client (streaming + non-streaming, model auto-detect)
│ └── prompts.py # System prompts (agent + chat-only), planner prompt, skill injection
├── tools/
│ ├── filesystem.py # read/write/append/delete/move/copy/list/mkdir/info
│ ├── search.py # search_files, grep_text, find_extensions, project_summary
│ ├── shell.py # run_command with timeout + safety gating
│ ├── git_tools.py # git status/diff/log/commit/branch
│ └── safety.py # dangerous-command detection, confirmation prompts
├── memory/
│ ├── session.py # current task, recent files, last plan, todos
│ ├── history.py # full chat transcript
│ └── skills.py # persistent user-defined skills
├── ui/
│ ├── tui.py # Textual application, splash screen
│ ├── panels.py # Rich panel builders, slash-command registry
│ ├── diff_view.py # unified diff rendering, patch approval
│ ├── themes.py # named color themes
│ └── glyphs.py # terminal Unicode capability detection, ASCII fallback
├── agent/
│ ├── intent.py # intent classification (CHAT/TASK/SYSTEM/TOOL_REQUEST)
│ ├── planner.py # produces a short plan via the LLM
│ ├── executor.py # plan → tool-call → answer loop; direct-response gate
│ ├── tool_router.py # parses tool-call JSON, dispatches to tools/
│ ├── project_instructions.py # reads an optional project-root PYCLAW.md
│ └── doctor.py # /doctor: configuration and connectivity audit
├── docs/
│ └── SKILLS.md # skills system documentation
├── examples/
│ └── skills/ # ready-to-use example skills
└── requirements.txt
"Offline" in the status bar. No model server is reachable at the
configured address. Start your server, or run /model to switch to a
different one. PyClaw rechecks automatically every ~15 seconds.
It shows qwen2.5-coder or another model name I never set. Older
versions of PyClaw defaulted model_name to a hardcoded value. Current
versions default to "auto" and auto-detect the real model from your
server; a one-time migration fixes this automatically for existing
config files the first time you launch.
Termux: the on-screen keyboard doesn't appear. This is a known Termux
limitation with full-screen terminal repaints, not specific to PyClaw.
PyClaw defaults to --no-tui on Termux for exactly this reason; if you
forced --tui and hit this, drop back to the default mode.
A box-drawing character shows as ? on Windows. This means your
terminal's encoding isn't UTF-8. PyClaw detects this and falls back to
plain ASCII borders/symbols automatically; if you still see ?
characters, your terminal may be misreporting its own encoding -- try
Windows Terminal instead of the legacy cmd.exe console.
Everything feels slow. Make sure context_size in /model roughly
matches the -c value your model server was actually started with -- a
mismatch can cause oversized requests. Also check /doctor for other
configuration issues.