5 releases
Uses new Rust 2024
| new 0.1.10 | May 22, 2026 |
|---|---|
| 0.1.9 | May 22, 2026 |
| 0.1.8 | May 21, 2026 |
| 0.1.7 | May 21, 2026 |
| 0.1.6 | May 21, 2026 |
#108 in Development tools
185KB
3.5K
SLoC
recast
CLI for safe, atomic, transparent multi-file text rewrites. Pure Rust. Tuned for LLM coding agents driving mechanical edits; equally usable by humans for mechanical refactors.
recast differs from sed / sd / a Python heredoc in five places:
- Match-required guard. Default
--at-least 1exits non-zero when nothing matches. Silent no-match is impossible by default. - Idempotency check. Refuses non-convergent rewrites; reports "already applied" on the second run.
- Atomic apply. Two-phase commit (sibling temp + fsync + rename)
with rollback if any per-file step fails. Crash-recovery sweep
reconciles leftover
.recast.bak/.recast.tmpsiblings. - Agent-friendly JSON.
--jsonemits a single-line, schema-locked report for plan / apply / check / error. - Three pattern modes. Regex (default), Rhai script callback
(
--script), or tree-sitter structural (--lang+--query/--ast).
Status: alpha (v0.1.6). All phases of PLAN.md landed.
- 📦 Install:
cargo install recast-cli(crates.io/recast-cli) - 📚 Library: crates.io/recast-core · docs.rs/recast-core
- 📥 Pre-built binaries: GitHub Releases
- 📖 Hosted docs: https://stoica-mihai.github.io/recast/
- 🛠 Operating manual:
AGENTS.md
Install
Pre-built binary
Download the matching artifact for your platform from Releases:
| OS | Targets |
|---|---|
| Linux x86_64 | x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl |
| Linux aarch64 | aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl |
| macOS x86_64 | x86_64-apple-darwin |
| macOS Apple Silicon | aarch64-apple-darwin |
The musl builds are statically linked — drop into an Alpine container
or distroless image without a glibc dependency.
Cargo install (from crates.io)
cargo install recast-cli # full feature set
cargo install recast-cli --no-default-features --features lang-rust # slim
The crate on crates.io is named recast-cli (the bare recast name was
already taken by an unrelated serialization-format library). The
installed binary is still called recast — that's the command name
everything in this README uses.
Cargo install (from source)
git clone https://github.com/Stoica-Mihai/recast
cd recast
cargo install --path crates/recast # full feature set
Stock install ships every grammar, the Rhai script engine, and JSON
output. Opt out via --no-default-features and pick only the features
you want (see § Cargo features).
Usage
recast [OPTIONS] <PATTERN> <REPLACEMENT> [PATHS]...
Diff preview (default)
recast 'OldName' 'NewName' src/
Prints a unified diff per file plus a summary line. No writes.
Atomic apply
recast --apply 'OldName' 'NewName' src/
Two-phase commit: each file is written to a sibling temp + fsync, then renamed into place. A failure mid-rename triggers reverse-rename of every already-renamed file from its backup, leaving the tree bit-identical to the pre-image.
CI gate
recast --check 'TODO' 'FIXME' .
# exit 0: nothing would change
# exit 1: at least one file would change
Capture groups
recast 'fn (\w+)_old\b' 'fn ${1}_new' src/
$1, ${name} interpolated; use --literal (-L) to disable
interpolation.
Filters
recast -t rust 'Old' 'New' . # only Rust files
recast -T markdown 'Old' 'New' . # everything except Markdown
recast -g '!vendor/**' 'Old' 'New' . # exclude vendor dir
recast --no-ignore 'Old' 'New' . # ignore .gitignore
recast --hidden 'Old' 'New' . # include dot-files
Stdin mode
echo 'fn old_name() {}' | recast --stdin 'old_name' 'new_name'
# fn new_name() {}
Reads one buffer, rewrites once, writes to stdout. Skips the walker and commit phases. The match-count guard still applies.
Scripted replacement (Rhai callback)
--script takes a path to a Rhai script that runs per match. The
script's return value (coerced to string) becomes the replacement. The
positional REPLACEMENT is still required but ignored — pass "".
cat > bump.rhai <<'RHAI'
(parse_int(captures[1]) + 1).to_string()
RHAI
echo "version 3" | recast --stdin --script bump.rhai '(\d+)' ''
# version 4
The script sees captures (array; index 0 is the full match) and
whole (full-match alias — match is a Rhai reserved keyword).
Structural rewrite (tree-sitter)
Two modes. Both require --lang <LANG>.
Friendly --ast — write the pattern in the target language with
$NAME (single-node) and $$$NAME (variable-shape subtree) placeholders:
recast --lang rust --apply \
--ast 'fn $NAME($$$ARGS) { $$$BODY }' \
'' \
'fn ${NAME}_v2$ARGS $BODY' \
src/
Matches any function regardless of param count or body shape; rewrites the name and keeps the original args/body verbatim.
Raw --query — pass a tree-sitter S-expression query directly:
recast --lang rust --apply \
--query '((identifier) @id (#eq? @id "old_name"))' \
'' 'new_name' src/
Capture named @root (or, absent that, the outermost capture in each
match) defines the byte range to replace. The template uses
$capture_name / ${capture_name} for substitution.
Supported languages
| Language | CLI name | Feature |
|---|---|---|
| Rust | rust, rs |
lang-rust |
| TypeScript | typescript, ts |
lang-ts |
| TSX | tsx |
lang-ts |
| JavaScript | javascript, js, jsx |
lang-js |
| Python | python, py |
lang-python |
| Bash | bash, sh, shell |
lang-bash |
| Go | go, golang |
lang-go |
| JSON | json |
lang-json |
| Markdown | markdown, md |
lang-md |
Crash recovery
If a --apply crashes mid-commit (panic, signal, power loss), the tree
may have leftover .foo.recast.bak.N / .foo.recast.tmp.N siblings.
Reconcile with:
recast --recover src/
Restores from backup when the target is missing; deletes stale temps and backups when the target is present.
Shell completions
recast --completions bash > /etc/bash_completion.d/recast
recast --completions zsh > ~/.config/zsh/completions/_recast
recast --completions fish > ~/.config/fish/completions/recast.fish
Also supported: elvish, powershell.
Agent-friendly JSON
recast --json --apply 'Old' 'New' src/
# {"kind":"apply","outcome":"changes","files_scanned":12,"files_written":3,"total_matches":7}
Schema documented in PLAN.md §7.1.
Snapshot-locked in crates/recast-core/src/snapshots/ — every
field-name / shape change is a visible PR diff.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success, or "no changes needed" |
1 |
--check set and at least one file would change |
2 |
Match-count guard violated (--at-least / --at-most) |
3 |
Internal error (regex parse, I/O, non-convergent pattern, script error, …) |
Cargo features
| Feature | Default | What it enables |
|---|---|---|
script |
✅ | Rhai script callback (--script) |
lang-rust |
✅ | Rust grammar for structural mode |
lang-ts |
✅ | TypeScript + TSX grammars |
lang-js |
✅ | JavaScript + JSX grammar |
lang-python |
✅ | Python grammar |
lang-bash |
✅ | Bash grammar |
lang-go |
✅ | Go grammar |
lang-json |
✅ | JSON grammar |
lang-md |
✅ | Markdown grammar |
lang-all |
✅ | Meta — enables every lang-* above |
Structural mode requires at least one lang-* feature. Drop the ones
you don't need to keep the binary lean:
cargo install --path crates/recast \
--no-default-features \
--features script,lang-rust,lang-ts
Build from source
cargo build --release --workspace --all-features
cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
136 tests on Linux + macOS. Proptest harness covers every public entry point with randomized input.
For AI agents (MCP server)
recast-mcp exposes the engine as a Model Context Protocol server
that MCP-aware agents (Claude Desktop, Cursor, Continue, Cline, …)
discover automatically through their tool registry. Same engine as
recast-cli, library-linked — no subprocess, no shell escaping, no
version skew.
cargo install recast-mcp
Add to your MCP client config (Claude Desktop example):
{
"mcpServers": {
"recast": { "command": "recast-mcp" }
}
}
Restart the client. Four tools become available:
| Tool | Purpose |
|---|---|
recast_preview |
Dry-run a regex rewrite, return plan + diffs. |
recast_apply |
Atomically apply a regex rewrite to disk. |
recast_structural |
Tree-sitter --ast rewrite (dry-run or apply). |
recast_recover |
Reconcile leftover .recast.bak.* / .tmp.* siblings. |
Why agents reach for it instead of write_file loops or sed:
default --at-least 1 guard turns silent zero-match runs into
errors, convergence check refuses non-idempotent patterns, two-phase
commit rolls back mid-failure, and every response is structured JSON
so the agent can branch on kind without string-matching error
messages.
Benchmarks
cargo bench -p recast-core --features lang-rust,script --bench engine
Criterion suite under crates/recast-core/benches/engine.rs measures
plan_rewrite, plan_structural_rewrite, pattern_compile, and the
structural rewrite hot path. HTML reports land under
target/criterion/.
Fuzzing
The fuzz/ crate (excluded from the workspace) holds cargo-fuzz
targets for the byte-walker / parser / compiler surfaces. Nightly +
cargo-fuzz required; see fuzz/README.md for
the target list and run instructions.
License
MIT. See LICENSE.
Dependencies
~13–31MB
~613K SLoC