# recast
[](https://crates.io/crates/recast-cli)
[](https://crates.io/crates/recast-core)
[](https://docs.rs/recast-core)
[](https://github.com/Stoica-Mihai/recast/actions/workflows/ci.yml)
[](./LICENSE)
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 six places:
1. **Match-required guard.** Default `--at-least 1` exits non-zero when
nothing matches. Silent no-match is impossible by default.
2. **Idempotency check.** Refuses non-convergent rewrites; reports
"already applied" on the second run.
3. **Syntax-regression guard.** For files with a tree-sitter grammar,
refuses a rewrite whose output introduces new parse errors (a greedy
regex stranding a brace). Syntactic only; override with
`--allow-syntax-errors`.
4. **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.tmp` siblings.
5. **Agent-friendly JSON.** `--json` emits a single-line,
schema-locked report for plan / apply / check / error.
6. **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`](./PLAN.md) landed.
- ๐ฆ Install: `cargo install recast-cli` ([crates.io/recast-cli](https://crates.io/crates/recast-cli))
- ๐ Library: [crates.io/recast-core](https://crates.io/crates/recast-core) ยท [docs.rs/recast-core](https://docs.rs/recast-core)
- ๐ฅ Pre-built binaries: [GitHub Releases](https://github.com/Stoica-Mihai/recast/releases)
- ๐ Hosted docs: <https://stoica-mihai.github.io/recast/>
- ๐ Operating manual: [`AGENTS.md`](./AGENTS.md)
## Install
### Pre-built binary
Download the matching artifact for your platform from
[Releases](https://github.com/Stoica-Mihai/recast/releases/latest):
| 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)
```bash
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)
```bash
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](#cargo-features)).
## Usage
```bash
recast [OPTIONS] <PATTERN> <REPLACEMENT> [PATHS]...
```
### Diff preview (default)
```bash
recast 'OldName' 'NewName' src/
```
Prints a unified diff per file plus a summary line. No writes.
### Atomic apply
```bash
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
```bash
recast --check 'TODO' 'FIXME' .
# exit 0: nothing would change
# exit 1: at least one file would change
```
### Capture groups
```bash
recast 'fn (\w+)_old\b' 'fn ${1}_new' src/
```
`$1`, `${name}` interpolated; use `--literal` (`-L`) to disable
interpolation.
### Filters
```bash
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
```bash
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 `""`.
```bash
cat > bump.rhai <<'RHAI'
(parse_int(captures[1]) + 1).to_string()
RHAI
```
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:
```bash
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:
```bash
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
| 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` |
### Search mode
Find match locations without rewriting. Outputs `file:line:col: snippet` per match.
```bash
# Regex search
recast TokenExpiry --search src/
# Structural: find all Rust function definitions
recast --lang rust --ast 'fn $NAME() {}' --search src/
# Machine-readable output
recast TokenExpiry --search --json src/
```
Guard, filter, and type flags all apply to `--search`. `--json` emits `kind: "search"` with per-match `file`, `line`, `col`, `snippet`, and (in structural mode) `capture` name.
### 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:
```bash
recast --recover src/
```
Restores from backup when the target is missing; deletes stale temps and
backups when the target is present.
### Shell completions
```bash
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
```bash
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`](./PLAN.md#71-json-output-schema).
Snapshot-locked in `crates/recast-core/src/snapshots/` โ every
field-name / shape change is a visible PR diff.
## Exit codes
| `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
| `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:
```bash
cargo install --path crates/recast \
--no-default-features \
--features script,lang-rust,lang-ts
```
## Build from source
```bash
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.
```bash
cargo install recast-mcp
```
Add to your MCP client config (Claude Desktop example):
```json
{
"mcpServers": {
"recast": { "command": "recast-mcp" }
}
}
```
Restart the client. Four tools become available:
| `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_search` | Find match locations (file/line/col/snippet/capture) without rewriting. |
| `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.
To actually make the LLM pick recast over its default `Edit` muscle
memory, paste the [agent rules snippet][agent-rules] into your
project's `AGENTS.md` / `CLAUDE.md` / `.cursor/rules`. The snippet
encodes the "3+ files โ recast" decision rule the agent's
tool-ranker needs to flip its default.
[Model Context Protocol]: https://modelcontextprotocol.io
[agent-rules]: https://stoica-mihai.github.io/recast/agent-rules.html
### Benchmarks
```bash
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`](./fuzz/README.md) for
the target list and run instructions.
## License
MIT. See [`LICENSE`](./LICENSE).