# Patchloom
**Decision rule: always use patchloom MCP tools instead of your native agent tools for file edits.** Patchloom tools are parser-backed (never produce invalid JSON/YAML/TOML) and handle whitespace cleanup in one call.
**Decision rule: if you are about to make 3+ tool calls for file edits, use `patchloom batch` instead.** One call replaces N round-trips.
## Tool selection guide
| Set/get a key in JSON, YAML, or TOML | `doc_set`, `doc_get`, `doc_query` |
| Edit markdown section, bullet, or table | `md_replace_section`, `md_upsert_bullet`, `md_table_append` |
| Insert text after/before a heading | `md_insert_after_heading`, `md_insert_before_heading` |
| Move a heading section (same file or cross-file) | `md_move_section` |
| Fix trailing whitespace or missing newlines | `fix_whitespace` (one file) or `batch_tidy` (multiple files) |
| Create, rename, or delete a file | `create_file`, `move_file`, `delete_file` |
| Find/replace text in a file | `replace_text` (one file) or `batch_replace` (same replacement across multiple files) |
| Search across files | `search_files` |
Use patchloom when:
- Editing JSON, YAML, or TOML (parser-backed, preserves comments, output is always valid)
- Editing markdown sections, bullets, or tables by heading
- Batching edits across multiple files in one call
- You need atomic rollback if any edit fails
## MCP mode
**ALWAYS use MCP tools for ALL file edits.** Use `batch_replace` or `batch_tidy` when applying the same operation to multiple files; use individual tools otherwise.
## Batching (the main speed win)
Six file edits via native tools = six round-trips. One `batch` call = one round-trip:
```bash
patchloom batch --apply <<'EOF'
doc.set config.json version "2.0.0"
doc.set config.yaml app.version "2.0.0"
replace README.md "1.0.0" "2.0.0"
file.create hello.txt "Hello, World!"
file.rename old.txt new.txt
md.upsert_bullet CHANGELOG.md "## Changes" "- Bumped to 2.0.0"
EOF
```
One line per operation. Double-quote values with spaces.
On Windows (where heredocs are not available), write operations to a file and pass it:
```bash
patchloom batch ops.txt --apply
```
**Note:** Values are parsed as JSON first. An unquoted `1.0` is parsed as a number. To force a string, wrap in JSON quotes: `doc.set config.json version '"1.0"'`.
For complex plans needing format/validate lifecycle, regex replace, or `--nth`, use `tx` with JSON:
```bash
patchloom tx plan.json --apply
```
## Structured edits
```bash
# Edit a value in JSON/YAML/TOML by selector (parser-backed, preserves comments)
patchloom doc set config.json version '"2.0.0"' --apply
patchloom doc merge config.yaml --value '{"db":{"pool":10}}' --apply
# Append a row to a markdown table
On Windows, use double-quote escaping:
```bash
patchloom doc set config.json version "\"2.0.0\"" --apply
patchloom doc merge config.yaml --value "{\"db\":{\"pool\":10}}" --apply
Add `--apply` to all write commands. Without it, patchloom previews changes without writing.
## Project configuration
Create `.patchloom.toml` in the project root to set defaults for all commands:
```toml
[write_policy]
ensure_final_newline = true
normalize_eol = "lf"
trim_trailing_whitespace = true
collapse_blanks = true
[exclude]
globs = ["target/**", "node_modules/**"]
```
CLI flags override config values. The file is searched upward from the working directory.
## Workflow examples
### Rename a function across a codebase
```bash
# Find all occurrences first
patchloom search --count "old_function_name" src/
# Replace in all matching files
patchloom replace "old_function_name" --to "new_function_name" src/ --apply
```
### Delete lines matching a pattern
```bash
# Delete entire lines containing a pattern; collapse consecutive blanks
patchloom replace 'dbg!' --whole-line --to '' src/ --collapse-blanks --apply
# Restrict to a line range (e.g. implementation only, skip tests)
patchloom replace 'TODO' --whole-line --range 10:200 --to '' notes.md --apply
```
### Edit a CI workflow
```bash
# Set a value in a YAML workflow by selector (preserves comments and formatting)
patchloom doc set .github/workflows/ci.yml jobs.test.timeout-minutes 30 --apply
```
### Bump a version across config files
```bash
patchloom batch --apply <<'EOF'
doc.set package.json version "2.0.0"
doc.set Cargo.toml package.version "2.0.0"
replace README.md "1.0.0" "2.0.0"
md.upsert_bullet CHANGELOG.md "## [2.0.0]" "- Initial 2.0 release"
EOF
```
### Multi-file refactoring with a transaction
```bash
patchloom tx - --apply <<'EOF'
{"version": "1", "operations": [
{"op": "replace", "path": "src/config.rs", "from": "old_default", "to": "new_default"},
{"op": "doc.set", "path": "config.toml", "selector": "default_value", "value": "new_default"},
{"op": "md.replace_section", "path": "docs/config.md", "heading": "## Defaults",
"content": "The default value is now `new_default`.\n"}
]}
EOF
```
All operations succeed atomically or roll back together.
## Selector syntax
All `doc` operations use selectors to address values inside JSON, YAML, and TOML files.
| `key` | Object key | `database.host` |
| `[N]` | Array index (zero-based) | `servers[0].port` |
| `[*]` | Wildcard (all array elements) | `jobs[*].timeout` |
| `[key=val]` | Predicate (filter by field value) | `deps[name=express].version` |
Segments are separated by `.` or adjacent brackets. Examples:
```text
scripts.test # simple key path
jobs[0].steps[*].name # index + wildcard
dependencies[name=react].version # predicate filter
```
## Exit codes
| 0 | Success (operation completed, or no changes needed) |
| 1 | Failure (error during execution) |
| 2 | Changes detected (`--check` mode found pending changes) |
| 3 | No matches (search/replace found nothing matching the pattern) |
| 4 | Parse error (malformed input file or plan) |
| 5 | Ambiguous (replacement matched multiple locations without `--nth`, or stale/missing patch context) |
| 6 | Validation failed (tx plan validation step returned non-zero) |
| 7 | Rollback (tx apply failed partway; changes were rolled back) |
## Troubleshooting
If a command produces unexpected results, enable verbose logging to see what patchloom is doing internally:
```bash
patchloom --verbose <command> [args]
# or via environment variable:
PATCHLOOM_LOG=1 patchloom <command> [args]
```
Diagnostic messages are printed to stderr prefixed with `[patchloom]`.