AST-native sed — find, replace, delete, and insert code by structure, not regex.
cargo install patchwork-cliInstall as a Claude Code plugin for AI-assisted code transformations:
/plugin install ThatXliner/patchwork# Rename a method across files without false positives
patchwork replace -i -p 'getOldData($a)' -r 'getData($a)' src/**/*.java
# Replace a logging framework
patchwork delete -i -p 'logger.debug($msg)' src/*.py
patchwork insert-before -p 'logger.debug($msg)' --code 'tracing.debug($msg)' src/*.py
# Match by structure, not regex
patchwork find -p 'return null;' src/More examples are available in the examples directory.
Most code transformation tools sit at extremes. Regex-based tools (sed) are fragile across multi-line patterns and nested syntax. Full-featured linters (semgrep, ast-grep) require config files, YAML rules, or heavy runtimes — overkill for the common case of "find this pattern, change it."
patchwork is for the middle ground: structural search and replace that runs as a single find | xargs pipeline. Zero config, zero setup, one 3MB binary.
ast-grep is more mature, supports more languages, has YAML rules, LSP, VS Code extension, MCP server, playground, pre-commit hooks, language bindings — the works. patchwork doesn't try to catch up on that axis. They go by different design philosophies.
| Tool | Language | Parsing | Size | Languages | Maturity |
|---|---|---|---|---|---|
| patchwork | Rust | tree-sitter AST | ~5MB | 13 | Alpha — one person, <1yr |
| ast-grep | Rust | tree-sitter AST | ~10MB | 25+ | Mature — 174 releases, active community |
| Comby | OCaml | parser-free | ~8MB | ~all | Mature |
| Semgrep | Python | real parsers | 200MB+ | 20+ | Mature — enterprise security product |
patchwork is built for scripts. Default output is intentionally boring (just file:line:col) because it's trivial to cut, xargs, or pipe into other tools. No config files, no YAML, no rule system — five flat commands with sed-like flags. If the only thing between your find and your edit is a tool that needs a config file, you reach for something else.
patchwork aims for more expressive matching. The Rust-style repetition syntax is the first step. We have $BODY/$STMT/$EXPR special tokens, a dedicated insert-before/insert-after command, and more matching logic planned — things that go beyond what a YAML rule system enables.
Two reasons:
-
The Rust-style repetition makes this tool similarly powerful as regex.
$f($($arg,)*)captures the separator and handles zero/one/many args with a natural Rust macro-like syntax. ast-grep'sconsole.log($$$ARGS)has no separator awareness — if you delete a single arg, you're left with trailing commas. -
Vision: a tool that AI agents reach for first. patchwork aims to be the simplest possible AST editor — so simple that an LLM can generate precise patchwork commands without thinking about YAML config, rule composition, or scanning modes. Whether it achieves this better than
ast-grep -p '...' -r '...'is an open question, but the design surface is intentionally tiny.
Realistically: if you need a mature, well-documented tool today, use ast-grep. If you're interested in the repetition syntax experiment or want to influence the design of a simpler alternative, watch this space.
Write a code snippet and patchwork finds structurally identical code in your source.
Names and values match exactly by default — return 1; only matches return 1;, not return 42;. This means you don't accidentally match code that happens to have the same shape but different identifiers.
# Only matches exactly this call
patchwork find -p 'old_api(x)' src/
# Use $ to match any identifier
patchwork find -p 'old_api($x)' src/$name matches any single AST node — any identifier, literal, or expression. It's like .* for code but structure-aware.
# Match any call to old_api with any single argument
patchwork replace -i -p 'old_api($arg)' -r 'new_api($arg)' src/**/*.java
# Match any two-argument call, reorder args in replacement
patchwork replace -i -p '$f($a, $b)' -r '$f($b, $a)' src/*.py
# Delete all calls to debug regardless of argument
patchwork delete -i -p 'debug($msg)' src/*.py
# Match any return statement
patchwork find -p 'return $val;' src/Inspired by Rust macro fragment specifiers, $name:Kind restricts a placeholder to match only nodes of a specific tree-sitter AST kind:
# Match only identifiers on the LHS (not obj.prop = ...)
patchwork find -p '$x:identifier = $val;' src/
# Match only string literal arguments
patchwork find -p 'log($msg:string_literal);' src/
# Match returns of integer literals only
patchwork find -p 'return $n:decimal_integer_literal;' src/The kind name is any tree-sitter node kind for that language — identifier, string_literal, decimal_integer_literal, method_invocation, binary_expression, etc.
Pre-defined shortcuts for common patterns:
| Token | Matches | Example |
|---|---|---|
$BODY |
Zero or more statements inside a block | if ($EXPR) { $BODY } |
$STMT |
A single statement of any kind | $STMT matches return 42;, if (x) {}, etc. |
$EXPR |
A single expression | debug($EXPR); matches debug(x), debug(f()) |
$BODY is statement-aware — it works where $$$name (see Advanced Patterns) doesn't because tree-sitter wraps bare identifiers in expression_statement nodes inside blocks. Use it to match arbitrary if/while/for bodies:
# Find all if statements regardless of body
patchwork find -p 'if ($EXPR) { $BODY }' src/
# Replace all debug calls with log calls
patchwork replace -i -p 'debug($EXPR);' -r 'log($EXPR);' src/**/*.javaFor precise structural patterns with named captures:
patchwork replace -q '(if_statement condition: (identifier) @matched)' -r 'check(x)' file.ts| Command | Effect |
|---|---|
find |
Print file:line:col for each match |
replace |
Replace matched code with new code |
delete |
Remove matched code |
insert-before |
Insert code before each match |
insert-after |
Insert code after each match |
nodes |
List available tree-sitter node kinds for a language |
All editing commands support -i (in-place, like sed -i) and stdin/stdout piping.
# Pipe mode (requires --language with stdin)
cat Main.java | patchwork find -l java -p 'return null;'
# File mode (language detected from extension)
patchwork replace -i -p 'println($arg)' -r 'log($arg)' src/**/*.java
# Query mode
patchwork find -q '(method_invocation name: (identifier) @name (#eq? @name "println"))' App.java
# Multi-file
patchwork replace -p 'BufferedReader $r' -r 'Reader $r' src/**/*.java
# List available tree-sitter node kinds for a language (discover $name:Kind options)
patchwork nodes java
patchwork nodes python
patchwork nodes java --all # include anonymous nodes (punctuation, operators)cargo install patchworkOr build from source:
git clone https://github.com/ThatXliner/patchwork
cd patchwork
cargo build --releaseJava, Python, JavaScript, TypeScript, TSX, Rust, Go, Ruby, C, C++, C#, PHP, Bash. Adding a language is one crate dependency and a handful of lines.
- Single-file only — no cross-file rename tracking or import updates
- Formatting — replacement text isn't auto-indented; include your own whitespace. This also means we don't really support Python
$$$namein blocks —$$$name(see Advanced Patterns) doesn't match statements inside blocks due to tree-sitter'sexpression_statementwrappers. Use$BODYinstead- No model — this is by design. Complex structural changes that need reasoning aren't supported. For those, use an LLM-based tool
Inspired by Rust macro repetition syntax, $($name)sep* matches repeated patterns with separators — like function arguments or array elements.
pattern: $f($($arg,)*)
matches: f(a, b, c) → captures $arg as a, b, c
f() → zero matches (allowed by *)
f(a) → captures $arg as a
The $(...) group wraps: an inner match pattern ($arg), a separator (,), and a quantifier (* = zero-or-more, + = one-or-more). The group must be at the last child position in your pattern.
Why separator awareness matters: $$$args captures a, b, c as one blob — remove one element and you're cleaning up commas by hand. $($arg,)* tracks each $arg independently, so the tool handles delimiters correctly.
# Match function calls with any number of arguments
patchwork find -p '$f($($arg,)*);' src/
# Match array literals
patchwork find -p '[$($elem,)*]' src/$$$name matches zero or more consecutive child nodes at any position. No separator tracking — it captures raw text.
# Match any method call with any number of arguments
patchwork find -p '$fn($$$args);' src/
# Match property chains
patchwork find -p 'users.$$$;' src/Like Rust macros, $(...) can contain multiple $name placeholders to match repeated compound patterns:
pattern: $f($($key, $val),*)
matches: f(x, 1, y, 2, z, 3) → captures $key as "xyz", $val as "123"
f() → zero groups, both captures empty
f(a, 0) → single group, $key="a", $val="0"
Each group of source children is matched against the placeholders in order. Each placeholder captures the concatenated text of its position across all groups.
# Match flat key-value pairs in function arguments
patchwork find -p '$f($($key, $val),*);' src/| Pattern | Position | Separator-aware | Best for |
|---|---|---|---|
$($name)sep* |
Last child only | Yes | Single repeated element (args, array items) |
$($a, $b)sep* |
Last child only | Yes | Repeated compound patterns (key-value pairs, binary ops) |
$$$name |
Any position | No | Method chains, arbitrary consecutive children |
$BODY |
Block body | N/A | Statements inside { } |
Single 3MB binary, zero dependencies, zero configuration. Works with pipes, files, and shell globs.