Static-analysis refactoring tools for Claude Code — Python (AST) and TypeScript (compiler API).
Status: early alpha. The tools work but the project is new — expect rough edges. Feedback and bug reports welcome.
Two MCP servers, one repo:
| Server | Language | Key advantage |
|---|---|---|
bonsai-py |
Python 3.10+ | AST-based, no type inference needed, zero extra deps |
bonsai-ts |
TypeScript / JavaScript | TypeScript compiler API — fully type-aware, no false positives from same-named methods |
Bonsai is part of a four-tool Claude Code suite:
| Plugin | What it does |
|---|---|
| bonsai (this repo) | AST-safe rename/move/find for Python & TypeScript |
| temper | Pre-commit diff reviewer |
| cairn | Commit narrator / CHANGELOG writer |
| whetstone | Pre-push quality gate |
bonsai/
├── py/ # Python MCP server
│ ├── src/bonsai_python/ # source modules
│ ├── tests/ # pytest suite + fixtures
│ └── pyproject.toml # Python packaging (hatchling)
├── ts/ # TypeScript MCP server
│ ├── src/ # TypeScript source modules
│ └── tests/ # vitest suite + fixtures
├── bin/
│ └── bonsai # CLI for install / uninstall / status / update
├── templates/
│ └── CLAUDE.md # snippet injected into ~/.claude/CLAUDE.md
├── scripts/ # dev helper scripts (setup / test / clean)
├── skills/ # Claude Code skill definitions
├── .claude-plugin/ # plugin manifest, hooks, marketplace config
└── .mcp.json # registers both MCP servers for local dev
git clone https://github.com/ValentinFigue/bonsai
cd bonsai
bash scripts/setup.shsetup.sh installs Python deps via uv (into py/.venv), builds the TypeScript server, and registers the enforcement hook at user scope in ~/.claude.json. sed, awk, grep, and find on .py/.ts/.tsx files will be blocked everywhere and redirected to the appropriate bonsai tool. Restart Claude Code to pick up the hook.
The .mcp.json at the repo root registers both servers from your local build.
Open the bonsai/ directory in Claude Code — both servers connect automatically.
Verify in Claude Code Settings → MCP: both bonsai-py and bonsai-ts should show ✓ Connected.
If a server fails to connect, test it manually:
uv run --directory py bonsai-py # Python server
node ts/dist/server.js # TypeScript serverThen restart Claude Code.
bash scripts/test.shOr per-language:
# Python only
cd py && uv run pytest tests/ -v
# TypeScript only
cd ts && npm test- Python: edit
py/src/bonsai_python/, re-run pytest. No server restart needed. - TypeScript: edit
ts/src/, runcd ts && npm run build. Restart Claude Code to pick up the new build.
To remove all local dev artifacts (virtual env, TS build output, and the local hook config):
bash scripts/clean.shTo also remove globally-registered MCP servers pointing to your local build:
claude mcp remove bonsai-py --scope user
claude mcp remove bonsai-ts --scope userThe .mcp.json only activates when you open the bonsai/ directory. To make both servers available globally (across all projects), run from the repo root:
./install.sh # registers MCP servers, hooks, skills, and the bonsai CLI
./install.sh --claude-md # same + injects the bonsai tool reference into ~/.claude/CLAUDE.mdVerify with bonsai status. Restart Claude Code once to pick up the changes.
When the packages are published and you want to switch to released versions, update .mcp.json to use uvx bonsai-py / npx --yes bonsai-ts and re-run bonsai update.
Clone the repo once, then run the install script from the repo root:
git clone https://github.com/ValentinFigue/bonsai
cd bonsai
./install.shOr pipe directly from GitHub (no clone needed):
curl -fsSL https://raw.githubusercontent.com/ValentinFigue/bonsai/main/install.sh | bashWith --claude-md, the installer also injects a bonsai reference table into ~/.claude/CLAUDE.md, which trains Claude to reach for AST tools automatically across all your projects:
./install.sh --claude-mdRestart Claude Code, then verify with:
bonsai statusbonsai install [--claude-md] Install (or re-install after a repo update)
bonsai uninstall [--claude-md] Remove everything bonsai installed
bonsai status Show what's installed / what's missing
bonsai enable-hook Enable the Bash nudge hook
bonsai disable-hook Disable the Bash nudge hook
bonsai update Rebuild servers and reinstall
bonsai is installed to ~/.local/bin/bonsai. If it's not on your PATH, add export PATH="$HOME/.local/bin:$PATH" to your shell profile.
/plugin marketplace add ValentinFigue/bonsai
/plugin install bonsai@ValentinFigue/bonsai
/bonsai:setup
- Line 1 registers this repo as a plugin marketplace (one-time).
- Line 2 installs the
bonsaiplugin from that marketplace — the@ValentinFigue/bonsaisuffix tells Claude Code which marketplace to resolve the plugin against. /bonsai:setupregisters both MCP servers (bonsai-pyandbonsai-ts) in~/.claude.json.
Then restart Claude Code.
| Tool | What it does |
|---|---|
pyfindrefs |
Find all usages of a class, function, or variable: definitions, imports, calls, decorators, base classes |
pycallers |
Find every call site of a function or method (call-type only) |
pyfindunused |
Detect dead top-level functions/classes, unused parameters, and unused imports |
pygrep |
Search for a text pattern (regex) across all Python files |
pymove |
Move or rename a Python file/package and rewrite all imports |
pymovesymbol |
Move a single function or class to a different module |
pyrename |
Scope-aware rename across the entire project |
pysignature |
Change a function's signature and update all call sites |
All Python tools use one of two forms for target:
module.submodule:SymbolName # dotted module path
module.submodule:ClassName.method # method on a class
path/to/file.py:SymbolName # file path (also accepted)
Examples: src.models:User src.models:User.save src/api/views.py:create_user
Find references
"Where is
Userused across the project?"
Claude calls: pyfindrefs("src.models:User")
Find callers
"What calls
PaymentService.charge?"
Claude calls: pycallers("src.services:PaymentService.charge")
Find dead code
"Find unused functions." "What imports are never used in
utils.py?"
Claude calls: pyfindunused(dead_code=True, imports=True)
Move a file
"Move
src/utils/helpers.pytosrc/core/helpers.pyand fix all imports."
Claude calls: pymove("src/utils/helpers.py", "src/core/helpers.py", dry_run=True), shows the diff, applies on confirmation.
Rename
"Rename
UsertoAccounteverywhere."
Claude calls: pyrename("src.models:User", "Account", dry_run=True)
Change a signature
"Add a
timeout: int = 30parameter tocreate_user."
Claude calls: pysignature("src.api:create_user", add=[{"name": "timeout", "type": "int", "default": "30"}], dry_run=True)
All mutating tools support dry_run=True. Claude uses dry-run by default and asks for confirmation.
- No type inference. Method attribution is best-effort:
pycallers("src.models:User.save")finds all.save()calls, including those on other classes. Use the TypeScript server for type-accurate method resolution. - No runtime analysis. Dynamic patterns like
getattr(obj, method)()are invisible to AST analysis. - Public symbols only for dead-code detection.
pyfindunusedskips private symbols, framework-decorated functions, and test files. - Single project tree. All tools operate on a project rooted at
pyproject.toml/.git. Passproject_rootexplicitly for monorepos.
| Tool | Python equivalent | Key capability |
|---|---|---|
tsfindrefs |
pyfindrefs |
Type-aware reference finder — no false positives from same-named methods |
tsrename |
pyrename |
Language Service rename — rewrites all imports, calls, type annotations in one pass |
tsmove |
pymove |
Move file/directory and rewrite all import paths project-wide |
tsmovesymbol |
pymovesymbol |
Move a single function/class; adds backward-compat re-export |
tssignature |
pysignature |
Change function signature and update all call sites |
TypeScript tools use path-style notation (maps directly to file paths):
path/to/module:Symbol # top-level symbol (no extension)
path/to/module:Class.method # method on a class
Examples: src/models/user:User src/models/user:User.save src/services:createUser
File extensions are stripped if provided: src/models/user.ts:User also works.
Find references (type-aware)
"Where is
User.savecalled?"
Claude calls: tsfindrefs("src/models/user:User.save")
Unlike pyfindrefs, this correctly distinguishes User.save from Document.save — it only returns call sites where the receiver is actually a User.
Rename
"Rename the
savemethod onUsertopersisteverywhere."
Claude calls: tsrename("src/models/user:User.save", "persist", dry_run=true)
Move a file
"Move
src/utils.tstosrc/helpers/utils.tsand fix all imports."
Claude calls: tsmove("src/utils.ts", "src/helpers/utils.ts", dry_run=true)
Move a symbol
"Move the
formatDatehelper fromsrc/utilstosrc/lib/dates."
Claude calls: tsmovesymbol("src/utils:formatDate", "src/lib/dates", dry_run=true)
A backward-compatible re-export is added to the original file — existing imports continue to work.
Change a signature
"Add an optional
timeout: number = 30tocreateUser."
Claude calls: tssignature("src/services:createUser", add=[{"name": "timeout", "type": "number", "default": "30"}], dry_run=true)
- Requires
tsconfig.jsonat the project root for full type resolution. Without it, the tools fall back to a glob-based project (reference finding works but loses type accuracy). - Dynamic
require()strings are not rewritten bytsmove. .d.ts-less packages: symbols from third-party packages without type declarations may be missed by reference finding.- TypeScript has no keyword arguments:
tssignaturereorder rewrites positional arguments at call sites (not named arguments). Combined remove + reorder operations apply reorder on post-removal argument positions.
All mutating tools accept dry_run=true. Always follow this pattern:
- Call with
dry_run=true— review the preview diff. - Confirm the blast radius (number of files, changed lines).
- Call again with
dry_run=false(orfalseby default) to apply.
Claude uses dry-run by default for all mutating operations.
Tip: Run bonsai tools on a clean git working tree (
git statusshows nothing uncommitted). This makes it trivial to review the full diff withgit diffand to undo withgit checkout .if anything looks wrong. The tools will warn you on stderr if uncommitted changes are detected.
First step: run bonsai status
This shows exactly which pieces are installed and which are missing — MCP server entries, permissions, hooks, skills, and the CLAUDE.md section.
Tools don't appear after install
Run bonsai status, fix anything flagged ✗, then restart Claude Code. If the servers still fail to connect, test them directly:
uv run --directory py bonsai-py # Python server
node ts/dist/server.js # TypeScript serverThen restart.
uvx not found
curl -LsSf https://astral.sh/uv/install.sh | shThen retry /bonsai:setup.
pyfindrefs / tsfindrefs returns "No references found" for a valid symbol
Pass project_root explicitly if your project is not at the current working directory:
pyfindrefs("src.models:User", project_root="/abs/path/to/project")
tsfindrefs("src/models/user:User", project_root="/abs/path/to/project")
tsfindrefs gives wrong results / misses references
Confirm your project has a tsconfig.json at the root. Without it, bonsai-ts falls back to a non-type-aware mode and may miss or misclassify references.
Add a [tool.bonsai] section to pyproject.toml to customise dead-code detection:
[tool.bonsai]
# Extend the built-in decorator exempt list (get, post, route, task, fixture, …)
dead_code_extra_decorators = ["api_view", "login_required"]
# Extend the built-in entry-point list (main, handler, lambda_handler, …)
dead_code_extra_entry_points = ["run", "execute", "on_ready"]
# Extend the built-in skip-dirs list (migrations, tests, alembic)
dead_code_extra_skip_dirs = ["fixtures", "scripts"]Each setting has an extra_* variant (merged with defaults) and a base variant (replaces defaults entirely).
MIT — see LICENSE.