Patchloom

Patchloom is a Rust CLI for structured file edits, built for AI coding agents.

AI agents are good at reasoning about code but bad at editing config files. When an agent needs to bump a version in config.yaml, it reaches for sed or text replacement. That works until the regex strips a YAML comment, breaks indentation, or produces invalid syntax. When the task touches six files, that means six separate tool calls, each a full round-trip back to the LLM. And on Windows, sed and jq do not exist.

Patchloom fixes all three problems with a single Rust binary.

What it does

  • Structured editing: Edit JSON, YAML, and TOML files by selector path, not regex. Comments and formatting are preserved because the file is parsed, not pattern-matched.
  • Batch operations: Bundle multiple file edits into a single tool call, cutting round-trips from six to one.
  • Cross-platform: Works identically on Linux, macOS, and Windows with zero dependencies.
  • Safe by default: All write operations preview changes without mutating files unless --apply is passed.
  • MCP server: Exposes all operations as structured tool calls for MCP-capable agents.

Quick example

# Edit a YAML value by selector path; comments and formatting survive
patchloom doc set config.yaml database.port 5432 --apply

# Version bump across 6 files in a single tool call
patchloom batch --apply <<'EOF'
doc.set package.json version "2.0.0"
doc.set config.yaml app.version "2.0.0"
replace README.md "1.0.0" "2.0.0"
EOF

24 commands

CategoryCommandDescription
TextsearchLiteral or regex search across files
replaceMechanical string replacement with diff preview
apply-fragmentFreeform fragment with required anchors (MorphLLM-style markers stripped; no cloud merge)
patchPreview or apply unified diffs
StructureddocParser-backed JSON, YAML, and TOML operations
mdMarkdown section-aware operations
CodeastAST-aware symbol operations (20 languages)
FilesappendAppend content to an existing file
prependPrepend content to the beginning of an existing file
createCreate a new file with content
deleteDelete a file
renameRename or move a file
readRead file contents with optional line range
statusShow uncommitted file changes
BatchtxExecute a multi-operation plan atomically
batchLine-oriented batch operations
NormalizetidyWhitespace, line ending, and final newline normalization
SafetyundoRestore files from backup
Agentmcp-serverMCP protocol server for structured tool calls
agent-rulesPrint agent rules for AGENTS.md
schemaExport operation schemas with tier filtering
explainExplain a tx plan in plain English
SetupinitSet up patchloom in the current project
completionsGenerate shell completions

As a Rust library

Patchloom is also a Rust library. Add it as a dependency to embed structured file editing in your own tools:

[dependencies]
patchloom = { version = "0.33.0", default-features = false } <!-- x-release-please-version -->

The api module exposes doc, replace, markdown, file, patch, multi-op content edits, and (with ast + files) AST rename / signature rewrite helpers. All API types are Send + Sync. Disabling default features omits the MCP server and its async dependencies.

Agent hosts should prefer ReplaceOptions::for_agent() so primary and fallback replace paths share one policy (unique, require_change, fuzzy at AGENT_MIN_FUZZY_SCORE 0.90, fail-closed allow_absent_old: false, and refuse_suspicious_fuzzy: true for over-wide fuzzy auto-refuse as FuzzySpanSuspicious; #1965 / #2005). Custom options still call api::fuzzy_span_suspicious after fuzzy Apply (#1981). Multi-op hosts use ContentEditsResult.op_honesty for per-replace pairing (#2006), or api::refuse_batch_if_suspicious_fuzzy after buffer multi-op (#2064). Opt-in command_position rewrites shell command tokens without touching arguments (including after sudo / timeout 30 / nice -n 10 / setsid / busybox / runuser / flock / chroot wrappers, across lines). The same replace options are available on the CLI (patchloom replace … --require-change --command-position), plan/MCP replace, and batch_replace. See the crate documentation and the library API table for the full surface.

Get started

Head to Installation to install, then follow the Quickstart to make your first edit.

Installation

Prefer channels we control. They track each GitHub Release within minutes: Homebrew, Scoop, crates.io, npm, and GitHub Release binaries/installers. On Windows, Scoop is still the recommended path. Community catalogs (winget, Chocolatey) remain useful for discovery and are fine when they match the version you need; see the winget/Chocolatey section for how each lags after a new GitHub Release.

brew install patchloom/tap/patchloom

This installs patchloom with all commands, including the MCP server.

After a new GitHub Release, formula metadata can show the new version while your linked cellar is still the previous one until you upgrade:

brew update
brew upgrade patchloom
patchloom --version
scoop bucket add patchloom https://github.com/patchloom/scoop-bucket
scoop install patchloom/patchloom

Each GitHub Release updates bucket/patchloom.json in patchloom/scoop-bucket with the new version and SHA256 hashes (same idea as the Homebrew tap). Then:

scoop update
scoop update patchloom
# One-shot (downloads the platform binary on first run)
npx patchloom --version

# Global install
npm install -g patchloom

The unscoped package is on npmjs.com/package/patchloom. It is generated by cargo-dist and downloads the matching prebuilt binary from GitHub Releases (not a Node rewrite of the CLI). Release CI publishes each new version via npm Trusted Publishing (OIDC from GitHub Actions; no long-lived write token required).

cargo install patchloom

Verify which binary and crate you have

Channels can disagree until you upgrade each one. Embedder hosts that pin patchloom in Cargo.toml / Cargo.lock often ship a library version ahead of the operator's shell PATH binary (Homebrew, Scoop, older cargo install).

SurfaceHow to check
CLI on PATHpatchloom --version
Homebrew formula / cellarbrew info patchloom/tap/patchloom then brew upgrade patchloom if linked is behind
Scoopscoop update patchloom then re-check --version
crates.io installcargo install patchloom (or cargo install-update -a if you use cargo-update)
Embedder library pinCargo.lock entry for name = "patchloom"

Before filing "CLI behavior differs from 0.x" bugs, compare patchloom --version to the version your host embeds. Dual-path dogfood (library + brew CLI) is expected to split until the shell binary is upgraded.

Release CI installs a fresh Homebrew formula and checks patchloom --version against the tag. That does not upgrade machines that already have an older cellar linked; run brew upgrade patchloom there.

Pre-built binaries for Linux (x64, ARM64, musl), macOS (x64, ARM64), and Windows (x64, ARM64) are available on the Releases page. Download the archive for your platform, extract, and place patchloom (or patchloom.exe on Windows) on your PATH.

Shell and PowerShell installer scripts are also available:

# Unix (Linux/macOS)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/patchloom/patchloom/releases/latest/download/patchloom-installer.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://github.com/patchloom/patchloom/releases/latest/download/patchloom-installer.ps1 | iex"

Pre-built binaries include all commands, including the MCP server.

Portable zip (Windows)

Each release ships flat zips (no nested folder). Common asset names:

  • patchloom-x86_64-pc-windows-msvc.zip (x64)
  • patchloom-aarch64-pc-windows-msvc.zip (ARM64)

Tag form is patchloom-vX.Y.Z (for example patchloom-v0.33.0).

$ver = "0.33.0"   # x-release-please-version (or pin an older release)
$tag = "patchloom-v$ver"
$url = "https://github.com/patchloom/patchloom/releases/download/$tag/patchloom-x86_64-pc-windows-msvc.zip"
$dir = "$env:TEMP\patchloom-portable"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
$zip = Join-Path $dir "pl.zip"
Invoke-WebRequest -Uri $url -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $dir -Force
# Layout: patchloom.exe, LICENSE, README.md, CHANGELOG.md at $dir root
& "$dir\patchloom.exe" --version
& "$dir\patchloom.exe" --help
# Optional: copy patchloom.exe to a directory already on PATH

winget and Chocolatey (Windows)

Prefer Scoop or a GitHub Release installer when you want the channel we operate. Use winget or Chocolatey when that is what your environment already allows. Both are published from Release CI; currency differs after each tag.

# winget (package id is Patchloom.Patchloom)
winget source update
winget install Patchloom.Patchloom
# or upgrade an existing install
winget upgrade Patchloom.Patchloom

# Chocolatey (community feed; may trail GitHub latest)
choco install patchloom
CatalogId / packageCurrency after a GitHub Release
wingetPatchloom.PatchloomRelease CI opens a microsoft/winget-pkgs PR. After Microsoft merge and publish, refresh with winget source update. Can lag by days while the PR waits; once published it is usually current.
ChocolateypatchloomEach nupkg is pushed automatically, then community moderation must approve it before it is installable as latest. Often trails GitHub longer than winget.

If winget show / choco list only offers an older build, that is expected until the community queue clears. Use Scoop or GitHub Releases for the version you just saw announced.

The files under chocolatey/ in this repository are a packaging template. Release CI rewrites the nuspec version and checksums before push; do not treat the in-tree version field as the live community feed version. Check community.chocolatey.org/packages/patchloom for what choco install actually resolves.

From source

Install from source (requires Rust 1.95+):

git clone https://github.com/patchloom/patchloom.git
cd patchloom
cargo install --path .

This builds with all features by default (CLI + MCP server + AST operations). To build a smaller binary without optional features (CLI is always included for the binary):

# CLI + AST only (no MCP server, no tokio/async)
cargo install --path . --no-default-features --features "cli,ast"

# CLI + MCP only (no AST grammars)
cargo install --path . --no-default-features --features "cli,mcp"

# CLI only (no MCP, no AST)
cargo install --path . --no-default-features --features cli

If you're contributing from a source checkout, use make check-fast while iterating and make check before committing.

As a Rust library

Add patchloom as a dependency to embed structured file editing in your own Rust tools. Disable default features to omit CLI (clap), MCP server, and AST:

[dependencies]
patchloom = { version = "0.33.0", default-features = false } <!-- x-release-please-version -->

To add AST support without CLI/MCP (LLM agent embedders typically use ast + files for plan execution and AST file mutators):

patchloom = { version = "0.33.0", default-features = false, features = ["ast", "files"] } <!-- x-release-please-version -->

With ast, 0.32 pulls tree-sitter 0.27 (links = "tree-sitter"). Cargo allows only one crate with that links key. Bump tree-sitter-highlight (or any other links = "tree-sitter" crate) to 0.27 in the same lock update. See Embedder host.

See the crate documentation for the full API surface (ReplaceOptions::for_agent, fuzzy_span_suspicious, require_change, command_position, ast_rename_batch, find_files_with_symbol, classify_error, restore_path_from_session, run_post_write_validation, match_mode) and the introduction for a quick overview. Host checklist: Embedder host. Embedder tables live under Library API in the reference.

Shell completions

After installing, generate shell completions:

# bash (system-wide; may require sudo and /etc/bash_completion.d in your setup)
patchloom completions bash > /etc/bash_completion.d/patchloom

# zsh (ensure ~/.zfunc is in $fpath, e.g. via oh-my-zsh custom or compinit)
patchloom completions zsh > ~/.zfunc/_patchloom

# fish
patchloom completions fish > ~/.config/fish/completions/patchloom.fish

# elvish
patchloom completions elvish > ~/.config/elvish/rc.elv

# PowerShell
patchloom completions powershell >> $PROFILE

Verify

patchloom --version
patchloom --help

Quickstart

This guide takes you from zero to a working multi-file edit in under 5 minutes.

Prerequisites

  • Patchloom installed (see installation.md)
  • A git repo to work in, or a test directory you can initialize with git init before Step 6

Step 0: Set up your project (optional)

Run init to generate agent rules, get shell completions, and detect MCP setup:

patchloom init

This creates AGENTS.md in a new project or appends the rules to an existing agent instructions file so AI agents know how to use patchloom. Pass -y to skip confirmation prompts. If .vscode/ or .cursor/ already exists, init also prints ready-to-copy .vscode/mcp.json or .cursor/mcp.json snippets.

Step 1: Search for something

Find all TODO comments in your project:

patchloom search 'TODO' src/

Count them:

patchloom search 'TODO' --count src/

Limit a search to a nested subtree with --glob:

patchloom search 'TODO' src/ --glob 'sub/*.rs'

Step 2: Replace text across files

Preview a rename (no files changed yet):

patchloom replace 'old_function' --new 'new_function' src/

The output shows a unified diff. When it looks correct, apply:

patchloom replace 'old_function' --new 'new_function' src/ --apply

If an agent emitted a freeform snippet with MorphLLM-style // ... existing code ... markers and you know a unique anchor, use apply-fragment (markers stripped; placement required) instead of rewriting the whole file. See Comparisons: Morph.

patchloom apply-fragment src/lib.rs \
  --after 'fn foo() {' \
  --fragment '// ... existing code ...
  bar();
// ... existing code ...' --apply

Step 3: Edit structured config

Read a value from a JSON file:

patchloom doc get package.json version

Set a new value:

patchloom doc set package.json version "2.0.0" --apply

Step 4: Batch a few file edits into one call

When you need several related edits at once, batch is the fastest path. This example assumes package.json, README.md, and CHANGELOG.md exist, and that CHANGELOG.md contains a ## Unreleased heading:

Preview the grouped edits:

patchloom batch <<'EOF'
doc.set package.json version "3.0.0"
replace README.md "v1.0.0" "v3.0.0"
md.insert_after_heading CHANGELOG.md "## Unreleased" "- Bumped to v3.0.0"
EOF

Apply them once the diff looks right:

patchloom batch --apply <<'EOF'
doc.set package.json version "3.0.0"
replace README.md "v1.0.0" "v3.0.0"
md.insert_after_heading CHANGELOG.md "## Unreleased" "- Bumped to v3.0.0"
EOF

On Windows / PowerShell

Classic PowerShell does not support bash heredocs (<<'EOF'). Write the ops to a file, or use a here-string piped into a temp file:

@'
doc.set package.json version "3.0.0"
replace README.md "v1.0.0" "v3.0.0"
md.insert_after_heading CHANGELOG.md "## Unreleased" "- Bumped to v3.0.0"
'@ | Set-Content ops.txt -Encoding utf8
patchloom batch --apply ops.txt

Cross-platform alternative: put the same ops in a JSON plan and run patchloom tx bump.json --apply (Step 5). Prefer tx or MCP when shell quoting is painful.

Step 5: Run an atomic transaction with a saved plan

Use tx when the change should live in a reusable plan file, or when you need format/validate lifecycle steps in the same transaction.

Create a plan file called bump.json:

{
  "version": 1,
  "write_policy": { "ensure_final_newline": true },
  "operations": [
    { "op": "doc.set", "path": "package.json", "selector": "version", "value": "2.0.0" },
    { "op": "replace", "path": "README.md", "old": "v1.0.0", "new": "v2.0.0" },
    { "op": "md.insert_after_heading", "path": "CHANGELOG.md", "heading": "## Unreleased", "content": "- Bumped to v2.0.0" }
  ]
}

Preview:

patchloom tx bump.json --diff

Apply all changes atomically:

patchloom tx bump.json --apply

If an operation fails, nothing is written. Format and validate lifecycle steps run after writes, so use "strict": true in the plan if you want those failures to roll back all changes too. Lifecycle failure output includes the failing step number, exit status, and the cwd used for that step.

Step 6: Explore code structure with AST

List all functions and types in a directory:

patchloom ast list src/

Filter by symbol kind:

patchloom ast list src/ --kind function,struct

Read a specific symbol's source code:

patchloom ast read src/main.rs run

Find all references to a symbol across files:

patchloom ast refs process_data src/

Validate syntax of source files:

patchloom ast validate src/

Generate a ranked repository map (PageRank over the symbol graph):

patchloom ast map src/ --max-tokens 2048

AST commands support 20 languages including Rust, Python, TypeScript, JavaScript, Go, Java, C#, C/C++, Ruby, PHP, Swift, Kotlin, HCL, and more.

Step 7: Inspect and undo changes

After any --apply, you can ask patchloom what changed and restore the latest backup session.

patchloom status is git-backed. If you're using a scratch directory, run git init, add the files you want tracked, and make an initial commit before this step.

See pending working-tree changes:

patchloom status

Preview what undo would restore (exit code 2 means files would be restored):

patchloom undo

Restore the most recent backup session:

patchloom undo --apply

Step 8: Use in CI

Check whether a plan would produce changes (exit code 2 = changes pending):

patchloom tx bump.json --check
echo $?  # 0 = clean, 2 = changes detected

Get machine-readable output:

patchloom --json tx bump.json --apply

Returns:

{
  "ok": true,
  "status": "success",
  "applied": true,
  "files_changed": 3,
  "files_created": 0,
  "files_deleted": 0,
  "changes": [
    { "path": "CHANGELOG.md", "action": "modified" },
    { "path": "README.md", "action": "modified", "match_mode": "exact", "match_count": 1 },
    { "path": "package.json", "action": "modified" }
  ],
  "match_mode": "exact",
  "match_count": 1
}

Troubleshooting

Config file not loading

Patchloom searches for .patchloom.toml starting from the working directory and walking up to the filesystem root. If your config does not seem to take effect:

  1. Verify the file location. Run from the directory containing .patchloom.toml or a subdirectory beneath it.

  2. Check for TOML syntax errors. Patchloom prints a warning to stderr when it finds a .patchloom.toml that cannot be parsed:

    warning: malformed /path/to/.patchloom.toml: expected `=`, found ...
    

    Validate your file with:

    patchloom doc get .patchloom.toml write_policy
    

    If this errors, fix the TOML syntax.

  3. CLI flags override config. Flags like --ensure-final-newline and --normalize-eol always take precedence over .patchloom.toml values.

Backups filling up disk

Backup sessions are stored under .patchloom/backups/ and are automatically pruned after 7 days. If you need to free space immediately:

rm -rf .patchloom/backups/

This is safe; the next --apply run will create a fresh backup directory.

Exit codes

CodeMeaning
0Success
1General failure (including invalid CLI usage)
2Changes detected (used by --check / write preview; not CLI usage errors)
3No matches found
4Parse error
5Ambiguous match
6Validation failed
7Rollback (transaction failed and was rolled back)
8Patch merge conflicts detected
9Tx operation staging failure

Next steps

  • Browse the examples directory for more tx plan patterns
  • See the full reference guide for command, operation, and notable mode guidance
  • Read concepts.md for write modes, exit codes, and glob filtering

Core Concepts

Commands

Patchloom has 24 commands:

  • search / replace -- text-level find and replace across files
  • apply-fragment -- freeform fragment with required anchors (MorphLLM-style // ... existing code ... markers stripped; no cloud model merge)
  • patch -- apply unified diffs
  • md -- markdown-aware editing (sections, bullets, tables, headings)
  • doc -- parser-backed JSON, YAML, and TOML mutations
  • tidy -- whitespace and line-ending normalization
  • append / prepend -- append or prepend content to an existing file
  • create / delete / rename -- file lifecycle
  • read -- file content inspection with optional line range (supports multiple files)
  • status -- uncommitted change summary from git
  • tx -- atomic multi-operation transactions
  • batch -- line-oriented multi-operation format (delegates to tx engine)
  • ast -- AST-aware operations (list, read, rename, validate) across 20 languages
  • completions -- shell completion generation
  • agent-rules -- print end-user agent documentation for patchloom
  • schema -- export operation schemas with tier filtering and system prompts
  • explain -- summarize a tx plan in plain English before applying
  • undo -- restore files from a backup created by --apply
  • init -- set up patchloom in a project (agent rules, completions, MCP)
  • mcp-server -- MCP protocol server exposing patchloom tools for AI agents

For feature-by-feature Use when guidance on commands, operations, and notable modes, see the reference guide.

For tool choice vs filesystem MCP, yq, ast-grep, and Morph, see Comparisons.

Context budget (agents)

Tool results cost model tokens. Prefer:

  • read with a line range instead of dumping large files
  • search --count / --files-with-matches before full content
  • batch / tx / multi-op plans instead of N sequential replaces
  • --jsonl for streaming many results
  • Treating sole binary / invalid UTF-8 as peels (error_kind), not soft no-match

Write modes

Every write command supports four modes:

FlagBehaviorUse case
default (preview)Show what would change without writing (exit 2 when changes would occur)Dry-run before --apply
--diffAttach/print a unified diff (combinable with --apply)Prefer explicit diff text
--checkExit 0 if clean, exit 2 if changes detectedCI pipelines, dry-run validation
--applyWrite changes to diskActual mutation
--confirmShow the diff, then prompt before writingInteractive preview-then-apply

--apply / --check / --confirm conflict with each other; --diff may combine with --apply. Patchloom is safe by default: nothing is written unless you pass --apply or confirm an interactive prompt.

Write safety on disk

Apply writers share a single atomic_write path:

  • Normal files (nlink == 1): write a same-directory temp file, then rename over the target so readers never see a half-written file.
  • Symlinks (#1230): resolve and write the target; the symlink directory entry is not replaced by a regular file.
  • Hardlinks (nlink > 1 / Windows nNumberOfLinks > 1, #1733): stage full content on a same-dir temp, then rewrite the existing inode in place so every hardlink path stays in sync. Temp+rename would break siblings (they would keep the old inode).
  • New files: create via temp + exclusive persist (no pre-existing inode to preserve).

Library embedders, CLI --apply, MCP tools, and tx/batch all use this path.

Write policy

A write policy controls transformations applied to all content before it reaches disk:

  • --ensure-final-newline -- non-empty files always end with \n
  • --normalize-eol <lf|crlf|cr> -- standardize line endings
  • --trim-trailing-whitespace -- remove trailing spaces on every line
  • --respect-editorconfig -- read policy from .editorconfig if present (newline, whitespace, and charset)

Standalone write commands use these flags directly. In tx, the same flags act as defaults for all writes, and plan-level write_policy entries override conflicting CLI flags for self-contained plans.

In tx plans, set these at the plan level:

{
  "version": 1,
  "write_policy": { "ensure_final_newline": true },
  "operations": [...]
}

Project configuration

Create a .patchloom.toml in your project root to set per-project defaults for write policy, tx, exclude, and output. [defaults] apply is ignored; write mode is --apply / --check / --diff / --confirm only. CLI flags override other config values.

[write_policy]
ensure_final_newline = true
normalize_eol = "lf"
trim_trailing_whitespace = true
collapse_blanks = true

[tx]
strict = false

[exclude]
globs = ["target/**", "node_modules/**"]

[output]
color = "auto"

The config file is searched from the working directory upward, so it works in subdirectories too.

Undo safety net

Before any --apply write, patchloom saves the original content of each affected file to .patchloom/backups/. If something goes wrong:

patchloom undo --list          # see available backups
patchloom undo                 # dry-run: show what would change
patchloom undo --apply         # actually restore files

Backups older than 7 days are auto-pruned.

Color output

Patchloom colorizes diffs and search results when stdout is a terminal. Override with:

  • --color=always -- force color (useful when piping to a pager like less -R)
  • --color=never -- disable color
  • NO_COLOR=1 -- environment variable that disables color for all tools (no-color.org)

Machine-readable modes (--json, --jsonl, --quiet) never produce color.

Transaction plans

The tx command runs multiple operations atomically. If staging fails because a target was not found (missing symbol, heading, or replace pattern), no files are written and the plan exits 3 (no_matches) with the concrete detail in the error message. Other staging failures exit 9 (operation_failed). If a write fails mid-commit, patchloom restores already-written files from the backup session (exit 7, rollback).

Plans are JSON objects with three lifecycle arrays:

  1. operations -- the mutations (replace, doc.set, md.replace_section, patch.apply, etc.)
  2. format -- shell commands that run after writes (e.g., cargo fmt)
  3. validate -- shell commands that verify correctness (e.g., make check)

patch.apply operations accept on_stale: "merge" for three-way merge when the on-disk file diverged from the patch base, and allow_conflicts: true to write conflict markers instead of failing.

With --json / --jsonl, plan results include file-level changes plus, for doc.delete / doc.delete_where, a mutations array and aggregate changed / removed counts (including removed: 0 for idempotent no-ops). The same fields appear on MCP write tools and execute_plan.

Strict mode defaults to on. Use "strict": false in the plan, [tx] strict = false in .patchloom.toml, or patchloom tx --no-strict to keep writes on disk when format/validate fails (exit 6). With strict mode, a format or validation failure reverts all writes (exit 7). If a write fails mid-commit, patchloom restores already-written files from the backup session (exit 7 rollback, or exit 1 rollback_failed if restore is incomplete).

Exit codes

Every command returns a specific exit code:

CodeMeaning
0Success
1General error (including CLI usage: invalid flags, enum values, missing args, unknown subcommands), or tx rollback_failed when mid-commit rollback could not fully restore files
2Changes detected (with --check or write preview; not used for CLI usage errors)
3No matches found
4Parse error in input
5Ambiguous (multiple replace matches, or stale patch context)
6Validation failed (writes may remain)
7Rollback (strict mode, no writes remain)
8Patch merge conflicts detected (apply blocked unless --allow-conflicts)
9Tx operation staging failure (operation_failed)

These codes let CI pipelines and agent frameworks branch on outcomes without parsing output.

When --json or --jsonl is set, CLI usage failures (invalid flags, enum values, missing required args, unknown subcommands) emit a JSON envelope on stdout with error_kind: "invalid_input" and exit 1. Without those flags, clap prints human usage text on stderr. Empty path arguments also set error_kind: "invalid_input". Path rejections under --contain (PathGuard) set error_kind: "guard_rejected" so agents can branch separately from usage errors (#1935).

When every explicit path root for search, replace, or tidy is missing (including a non-stdin --files-from list), exit 1 with error_kind: "not_found". That all-missing message names the --files-from list (not each dest). Pattern misses on existing files still use exit 3 (no_matches). Empty existing directories remain clean success for tidy. On Windows, a drive-relative or root-relative --files-from line peels invalid_input of that dest before the missing-root check.

Glob filtering

Most commands accept --glob <pattern> (repeatable) to restrict which files are processed:

patchloom replace "old" --new "new" --glob "*.rs" --glob "*.toml" --apply

Glob patterns match either the basename or the path relative to the input root. For example, if you search src/, then --glob 'sub/*.txt' matches src/sub/file.txt.

In tx plans, individual operations can use "glob" instead of "path" to target multiple files.

Security model

Patchloom runs with the privileges of the invoking user and treats all inputs (command-line arguments, plan files, stdin) as trusted. This is the same trust model as make, sh, or cargo.

What this means in practice:

  • Plans can execute arbitrary shell commands. The format and validate lifecycle steps pass their cmd field to sh -c (or cmd /C on Windows) with the user's full privileges. Only load plans you trust. MCP strips these steps. Library execute_plan with a PathGuard, and CLI tx/batch with --contain, refuse redirects, pipelines, and substitutions before commit. Hosts can also call api::lifecycle_cmds + api::refuse_lifecycle_shell_metas.
  • CLI file operations are unrestricted by default. create, delete, read, search, replace, patch, rename, ast, md, doc, tidy, status, and all tx operations accept any path the invoking user can access (including ../ escapes from --cwd). --cwd only sets the default base for relative paths; it is not a containment boundary unless you also pass --contain, which enables PathGuard for CLI reads, writes, and meta-input files (tx/explain plans, batch ops files, patch files, and --files-from lists). On Windows, drive-relative dests (C:foo) and root-relative dests (\foo) are refused as invalid_input because they ignore --cwd. Under --contain, absolute paths that resolve inside the workspace are allowed (AllowIfContained); ../ escapes and absolute paths outside the workspace are rejected. MCP uses the same in-workspace absolute-path rule for tool path arguments (AllowIfContained): prefer relative paths, but absolute paths under the server root are accepted so agents can join server_info.cwd + relative. Outside-workspace absolute paths and ../ escapes still fail closed.
  • --contain follows effective --cwd (#1832). Containment is relative to --cwd if set, else the process cwd. An agent that can pass both flags can re-root with --cwd .. and then write "inside" paths that were outside the original project. Hosts that shell out for agent sandboxes must pin --cwd <project> --contain themselves and strip or ignore model-supplied --cwd / --contain overrides.
  • MCP and the library PathGuard are sandboxed. The MCP server and embedders that pass a PathGuard reject paths that escape the workspace root (via ../ or symlinks). Prefer MCP tool calls when an agent must stay inside a workspace, or use host-pinned patchloom --cwd <ws> --contain … for CLI agents.
  • Plan cwd overrides the working directory. A plan's cwd field changes the working directory for all subsequent operations and lifecycle steps. Relative values resolve from the invocation root, not from the plan file location. In normal CLI use this still runs with the invoking user's filesystem access. In MCP mode, cwd must be a relative path under the server workspace (honored for re-rooting; absolute path strings and ../ escapes are rejected). Do not combine cwd with for_each on MCP.

For AI agent authors: Prefer the MCP server for agent-driven edits so path containment is always on. If the agent shells out to the CLI instead, the host must invoke patchloom --cwd <workspace> --contain on every write and must not forward model-chosen --cwd values (#1832). Do not construct plans from untrusted conversational input without validation. A plan is equivalent to a shell script. Treat plan files with the same care you would treat a Makefile or a bash script from an unknown source.

Comparisons and when to use what

Patchloom is not a generic filesystem MCP and not a drop-in replacement for full coding agents. Use this page to choose tools.

vs official / generic MCP filesystem

CapabilityGeneric filesystem MCPPatchloom
Read / write / list filesYesYes (read, create, delete, …)
Dry-run / preview before writeRareDefault dry-run; exit 2 when changes would apply
JSON / YAML / TOML by selectorNo (text edit)doc (parser-backed; multi-doc YAML honesty)
Markdown section / table / bulletNomd
AST rename / symbol opsNoast
Multi-file atomic plan + undoNotx / batch + undo
Agent-branchable error_kindWeakbinary, invalid_encoding, already_exists, format_failed, …

Use a generic filesystem MCP only when you need pure FS ops and Patchloom is not installed.

Use Patchloom alone for list + search + structured edit: MCP list_files covers inventory (ignore-aware, capped), so coding agents should not pair Patchloom with a second filesystem MCP for list/read/edit. Prefer PATCHLOOM_MCP_SURFACE=core (11 tools including list_files).

Install notes: MCP setup (Cursor / Claude / Codex paste configs with core). Registry name: io.github.patchloom/patchloom.

vs yq / dasel / jq

Capabilityyq / daselPatchloom
Selector mutate JSON/YAML/TOMLYesYes (doc)
Same tool for md / AST / replace / txNoYes
Agent JSON + exit codesShell stderrStable error_kind, dry-run exit 2
Multi-document YAML stream honestyLimitedBare-key type_error; 0.key / [0]
MCP + Rust library host contractsNoMCP + ReplaceOptions::for_agent

Use yq/dasel in human scripts and one-off shell.

Use Patchloom inside agent loops (CLI, MCP, or embedder library).

yq one-liners → Patchloom (agent cheat sheet)

Shell habitPatchloom
yq '.version' package.jsonpatchloom doc get package.json version or MCP doc_get
yq -i '.version = "2.0.0"' package.jsonpatchloom doc set package.json version '"2.0.0"' --apply (or MCP doc_set)
yq 'select(document_index == 0) | .a' multi.yamlpatchloom doc get multi.yaml 0.a
yq -i '.[0].image = "x"' multi.yamlpatchloom doc set multi.yaml 0.image x --apply
yq '(.items[] | select(.name == "a")).v = 99' -i f.yamlpatchloom doc update f.yaml 'items[name=a].v' 99 --apply (or MCP doc_update); not doc set
yq '.items[] | select(.name == "a")' f.yamlpatchloom doc select f.yaml items --predicate name=a (or MCP doc_query / plan)

doc set vs doc update (agent DX)

Selector shapeUse
Concrete path (server.port, items.0.v, items[0].v)doc set / plan doc.set / MCP doc_set (also doc ensure for idempotent set)
Predicate or wildcard multi-match (items[name=a].v, items[*].enabled)doc update / plan doc.update / MCP doc_update

doc set is single-path only. Predicate selectors fail closed with invalid_input and a message that points at doc update. Prefer the right op on the first try so agents do not burn a turn.

Prefer parser-backed doc over inventing yq in agent shells so peels, dry-run exit 2, and multi-doc honesty stay consistent.

vs ast-grep (complement)

ast-grep owns structural code search and pattern rewrite (syntax-tree patterns, codemods). Patchloom owns host-safe apply for configs, markdown, multi-file transactions, and agent honesty.

TaskPrefer
Find every call matching a code shapeast-grep (or ast search / project tools)
Rename a symbol with project-aware ASTPatchloom ast rename / ast_rename_project
Set database.port in YAML without losing commentsPatchloom doc set
Atomic multi-file apply with undoPatchloom tx / batch
After fuzzy text replace, refuse over-wide spans (library host)Patchloom fuzzy_span_suspicious / batch refuse_batch_if_suspicious_fuzzy

Typical pipeline (ast-grep + Patchloom)

  1. Discover code shapes with ast-grep (or patchloom ast search when a simple structural query is enough).
  2. Apply with Patchloom so peels and undo stay host-safe:
    • identifier rename: ast rename PATH --old X --new Y --apply or plan ast.rename
    • multi-file atomic apply: tx / MCP execute_plan
    • config/docs alongside code: doc set / md replace-section in the same plan
  3. Do not re-apply the same edit with raw sed after Patchloom already wrote.

Example: ast-grep finds call sites; Patchloom ast rename + tx with require_change applies and fails closed if a path misses.

vs Morph Fast Apply (and similar merge APIs)

Morph (Fast Apply) is a cloud apply model: the agent emits a short snippet (often with // ... existing code ... markers); Morph merges into the file at high token rates. That optimizes whole-file rewrites inside full agent loops.

Morph Fast ApplyPatchloom
ExecutionNetwork APILocal binary / library
DeterminismModel mergeParser / exact / controlled fuzzy
Configs / multi-doc YAML / md sectionsNot the product focusCore
Dry-run exit codes + peelsN/ACore
Offline / air-gappedNoYes

Interop (no integration required): a host may use Morph (or IDE apply) for freeform code and Patchloom for structured configs, plans, and peels. Patchloom does not depend on paid apply APIs for its core path.

Morph job migration (verified)

Use this table when someone asks "can patchloom replace Morph for X?" Full PASS/PARTIAL/GAP matrix with re-run commands: morph-gap-matrix.

Morph jobPrefer PatchloomNotes
Small exact editreplace OLD path --new NEWExact; dry-run then --apply
Whitespace / near-missreplace … --fuzzyDefault fail-closed (reports matched_text, no write). Hosts: for_agent + fuzzy_span_suspicious / buffer multi-op refuse_batch_if_suspicious_fuzzy; optional --allow-absent-old only when you accept a nearby span
Large file, known line/symbolreplace or ast replace PATH SYMBOL --old --newScope to a symbol when possible
Scattered multi-hunk / multi-filebatch / tx / library apply_content_edits*One plan, atomic undo
Config / commentsdoc setNot text replace
Markdown sectionmd replace-section (etc.)Not whole-file rewrite
Preview / revertdefault dry-run (exit 2); undo --applyBackup session on apply
Lazy // ... existing code ... with known anchorapply-fragment --after/--before/--old (markers stripped)PASS (#2018). No Morph-style model merge
Lazy markers with no anchorsNot supportedNon-goal: supply after/before/old or use Morph
Freeform "put method in the right place" with known anchorapply-fragment or ast + insertPASS with anchors; no free placement guess

Host routing (Morph MCP says "prefer edit_file"): prefer doc / md / ast / batch when structure is known; use replace (+ fuzzy only when needed); use apply-fragment only with a known after/before/old anchor; never whole-file rewrite for a one-line change.

Non-goals (do not expect Patchloom to replace Morph here):

  • Anchor-less lazy snippet merge (// ... existing code ... with no placement)
  • Competing on cloud apply tok/s or network Fast Apply latency

vs full coding agents (Claude Code, Codex, Cursor, Aider)

Those products own the agent loop. Patchloom is a tool layer (CLI / MCP / library) they can call. Do not install Patchloom expecting to replace the agent; install it so the agent edits structure safely.

Context budget tips

Agents pay for tokens on every tool result. Prefer:

  • read with a line range instead of dumping huge files
  • search with --count / --files-with-matches (and limits when available) before full content
  • batch / tx / multi-op content edits instead of N sequential replaces
  • --jsonl when streaming many results
  • Soft-skip binary paths; sole binary targets return error_kind: binary

See also Core concepts and agent-rules output from patchloom agent-rules.

Embedder host checklist (Rust library)

For LLM agent hosts / embedders that call Patchloom as a library (not only shell out to the CLI). Public API: docs.rs/patchloom.

This is the one-screen ordered checklist for a first host integration. Bline is a real embedder that dogfooded these steps; the checklist is host-generic.

Version dual-path: a Cargo pin of patchloom (library) can be ahead of the operator shell binary (brew / scoop / old cargo install). When dogfooding CLI and library side by side, run patchloom --version and compare to Cargo.lock before treating CLI exit codes as a library regression. See Installation: verify which binary.

0.32 tree-sitter graph: features = ["ast"] pulls tree-sitter 0.27 (links = "tree-sitter"). Cargo allows only one crate with that links key. A host that still pins tree-sitter-highlight ^0.26 (or another links = "tree-sitter" crate at 0.26) cannot cargo update -p patchloom. Bump those crates to 0.27 in the same lock update. Highlighter::highlight already takes cancellation_flag in 0.26; after the highlight 0.27 bump, check host tree-sitter call sites (Parser::parse, QueryMatch::captures). 0.32 does not keep a 0.26 tree-sitter graph.

doc set vs doc update: single concrete paths use doc.set / MCP doc_set; predicate or wildcard multi-match uses doc.update / doc_update. Do not expose only doc_set if agents need list updates by name. See Comparisons.

Minimal checklist

  1. PathGuard from the workspace root (and allow_temp_directory if the agent writes under /tmp). Pass Some(&guard) into Apply writers. Blank or whitespace-only paths fail closed as ContainmentError::EmptyPath (path must not be empty); they do not resolve as the workspace root. Match ContainmentError with a _ arm: the enum is #[non_exhaustive] (0.28.0).
  2. Sole-path text load: api::load_text / load_text_strict (or is_binary_file preflight). Peel Binary / InvalidEncoding / is_load_text_strict_fail instead of scraping English.
  3. Dual-path replace (primary + fallback): call ReplaceOptions::for_agent() in both places. Do not hand-copy ReplaceOptions { ... } twice (options drift is a common footgun). Preset: unique, require_change, fuzzy at AGENT_MIN_FUZZY_SCORE (0.90), allow_absent_old: false, refuse_suspicious_fuzzy: true.
  4. Over-wide fuzzy: with for_agent(), refuse is automatic (EditErrorKind::FuzzySpanSuspicious / is_fuzzy_span_suspicious). For custom options, call fuzzy_span_suspicious(old, matched_text, score) or fuzzy_span_suspicious_with_policy + FuzzySpanPolicy before trusting Apply.
  5. On Err: branch with edit_error_kind / error_kind_str / is_* peels. Keep a _ arm: EditErrorKind is #[non_exhaustive].
  6. Apply writers: prefer api file_* / replace_text / apply_content_edits_to_file (hardlink preserve, backup_session). Persist EditResult.backup_session on success if the host exposes undo. On Err after write/backup (FormatFailed or fail-restore), use api::backup_session_from_error(&err) for the same session id without scraping English Display (#2127).
  7. Multi-op / multi-path honesty:
    • Buffer multi-op: apply_content_edits + ContentEditsResult.op_honesty (per-replace old + matched_text + match_score; #2006).
    • Buffer multi-op with host-owned write: after a successful batch, call refuse_batch_if_suspicious_fuzzy(&batch, &FuzzySpanPolicy::default()) before trusting batch.modified (#2064). Same kind as single-op refuse; only Fuzzy honesty rows are checked. Prefer this over reimplementing the loop with fuzzy_span_suspicious alone.
    • Plan/tx multi-path: top-level widest matched_text + min fuzzy score (#2007); pair refuse with changes[] / plan old, not unpaired rollup fields.
    • Disk multi-op with a final gate:
      apply_content_edits_to_file_with_span_policy(path, edits, mode, guard, Some(&FuzzySpanPolicy::default()))
      refuses over-wide fuzzy before write/backup (#2008).
  8. Path-only file ops on non-text: file_rename / file_delete succeed on binary and invalid UTF-8 with byte backup and PathGuard (no OS dual-path) (#2031). Both also handle FIFO/socket/device and symlinks (including dangling and symlink-to-dir) as directory-entry moves/unlinks without following the target; directories stay refused (#2087, #2091). Soft-loading a symlink as text then writing would rewrite the target; rename uses an empty path-only snapshot so write policies never mutate the link target. Entry containment (#2115): delete and path-only rename use PathGuard::check_path_entry (parent follows; final component does not). A workspace link whose target is outside the root can be unlinked or renamed under a workspace guard without parent-only host workarounds or guard: None. Content writes (replace, doc_*, append) still use follow-mode check_path. Append/prepend refuse non-text and special entries (including dangling symlinks) with invalid_input (not not_found). Sole explicit replace/search/tidy paths use the same classification via sole_explicit_non_text so agents do not mis-branch on not_found for a present non-file entry.
  9. Doc presentation: library EditResult.style_changed (and is_style_changed) mirrors CLI/MCP when YAML block-sequence layout collapses or when & / * / <<: identity is dropped; values can still be correct (#2088). Warn hosts/agents; do not treat as failure.
  10. Morph-class freeform on disk: apply_fragment_to_file(path, fragment, FragmentPlacement::After|Before|Replace(...), unique, mode, guard) strips lazy markers and applies via the replace path (#2032).
  11. Host unit tests for op_honesty: ContentEditHonesty is #[non_exhaustive]; use ContentEditHonesty::exact / ::fuzzy instead of struct literals (#2033). Prefer live apply_content_edits for integration tests.
  12. Codex Begin Patch: call apply_patch / apply_patch_file (they detect *** Begin Patch). Dest-deny first with looks_like_begin_patch + begin_patch_declared_paths. Do not copy a Begin Patch parser. Mixed Begin Patch + unified-diff is a typed error. Update hunks require a unique exact match (#2219).
  13. SEARCH/REPLACE / DiffFenced: parse_search_replace / apply_search_replace_blocks (or apply_search_replace_document). apply_patch / apply_patch_file also detect <<<<<<< SEARCH. Dest-deny with looks_like_search_replace + search_replace_declared_paths. Default is unique: multi-match is ambiguous and does not write. Pass replace_all: true (CLI patch apply --replace-all, MCP replace_all) to update every exact match. Empty SEARCH is invalid input. Do not replacen(..., 1) or raw fs::write, and do not flip ReplaceOptions.unique on generic replace_text (#2220 / #2221).

Minimal sketch

#![allow(unused)]
fn main() {
use patchloom::api::{
    replace_in_content, ReplaceOptions, edit_error_kind, EditErrorKind,
    is_fuzzy_span_suspicious, ApplyMode, apply_content_edits,
    apply_content_edits_to_file_with_span_policy, refuse_batch_if_suspicious_fuzzy,
    ContentEdit, FuzzySpanPolicy,
};
use patchloom::containment::PathGuard;
use std::path::Path;

// 1) Containment (optional but recommended for sandboxed agents)
let guard = PathGuard::builder(std::env::current_dir()?).build()?;

// 2–5) Dual-path replace: same for_agent() on primary AND fallback call sites
let opts = ReplaceOptions::for_agent();
match replace_in_content(content, old, new, &opts) {
    Ok(r) => { /* refuse_suspicious_fuzzy already ran for fuzzy */ }
    Err(e) => {
        if is_fuzzy_span_suspicious(&e) {
            return Err(e); // over-wide fuzzy
        }
        match edit_error_kind(&e) {
            Some(EditErrorKind::NoMatch) => return Err(e),
            Some(EditErrorKind::Binary) => return Err(e),
            Some(_) | None => return Err(e), // non_exhaustive: keep _
        }
    }
}

// 6–7a) Disk multi-op Apply with optional pre-write span policy
let edits = [ContentEdit::Replace {
    old: old.into(),
    new: new.into(),
    options: ReplaceOptions::for_agent(),
}];
let policy = FuzzySpanPolicy::default();
let _ = apply_content_edits_to_file_with_span_policy(
    Path::new("notes.txt"),
    &edits,
    ApplyMode::Apply,
    Some(&guard),
    Some(&policy),
)?;

// 6–7b) Buffer multi-op + host write: public batch refuse (#2064)
let batch = apply_content_edits("notes body", &edits)?;
refuse_batch_if_suspicious_fuzzy(&batch, &policy)?;
// host write of batch.modified (hardlinks, custom backup, …)
}

Approximate recovery stays an explicit override:

#![allow(unused)]
fn main() {
ReplaceOptions {
    allow_absent_old: true,
    ..ReplaceOptions::for_agent()
}
}

(not a second constructor; #1980). To keep approximate recovery without span auto-refuse: also set refuse_suspicious_fuzzy: false.

Multi-op notes

apply_content_edits rolls up the widest matched_text and the minimum fuzzy score independently (may be different ops). Prefer op_honesty for refuse pairing, or call refuse_batch_if_suspicious_fuzzy for the full batch gate (#2064). When each replace uses for_agent(), over-wide fuzzy fails inside that op (all-or-nothing batch). Plan/tx multi-path top-level honesty matches that worst-case rollup (#2007).

Multi-file apply_patch_file: preflights every hunk, then one backup session for every path (including creates and rename destinations). Mid-batch write failure restores the whole session (no orphan creates or half-renames). Prefer this (or plan/execute_plan patch ops) over looping single-file apply_patch when a unified diff touches multiple files. An empty-hunk git delete of a missing dest peels not_found on Apply and Check (same as apply_patch / file_delete); do not treat it as changed: true. A directory dest peels invalid_input (same as file_delete). FIFO, dangling symlink, and binary regular files stay path-only unlinkable (same as file_delete). With a PathGuard, an escaped missing dest (../gone.txt) peels guard_rejected first, then in-workspace missing dests peel not_found.

Plan for_each and lifecycle commands

for_each expansion requires the files feature (the CLI feature already enables files). It does not require cli. Call api::expand_for_each(&mut plan, cwd) before walking Operation::declared_paths(), or rely on execute_plan, which expands before PathGuard (#2169). Zero-match globs are NoMatch. Unparseable glob/exclude and a filter other than has_symbol(NAME) are InvalidInput. Do not combine plan.cwd with for_each.

Plan format / validate steps are raw shell (sh -c / cmd /C). MCP execute_plan still strips them (#1142). Library hosts that pass Some(&PathGuard) get an automatic refuse of redirects, pipelines, and substitutions before commit (EditErrorKind::GuardRejected). Hosts that call execute_plan with None should preflight:

#![allow(unused)]
fn main() {
use patchloom::api::{lifecycle_cmds, refuse_lifecycle_shell_metas};

for cmd in lifecycle_cmds(&plan) {
    refuse_lifecycle_shell_metas(cmd)?; // InvalidInput on `|`, `>`, `$`, …
}
}

true, cargo fmt, and rustfmt with no metas still run. This is not a POSIX shell parser.

Patch dest preflight

parse_unified_diff C-unescapes --- / +++ / rename / copy dests and lists git-meta dests that apply refuses (binary payload, Binary files differ, mode-only chmod). Hosts that deny secret names (.env) before apply_patch_file must use the shared helpers, not quote-peel or whitespace-split diff --git:

#![allow(unused)]
fn main() {
use patchloom::api::{
    parse_diff_file_path, parse_diff_git_paths, patch_declared_paths,
    unquote_git_c_string,
};

assert_eq!(unquote_git_c_string(r"\056env"), ".env");
assert_eq!(parse_diff_file_path(r#"+++ "b/\056env""#), ".env");
// Full line or the pair after `diff --git ` (prefix optional).
let (a, b) = parse_diff_git_paths(r#""a/notes.txt" "b/.env secret""#).unwrap();
assert_eq!((a.as_str(), b.as_str()), ("notes.txt", ".env secret"));
let dests = patch_declared_paths(diff_text)?;
}

Git 100% copy from / copy to creates the dest and keeps the source. Dest-exists without force peels AlreadyExists. Mixed patches no longer drop copy / binary / empty-create dests. Empty-create apply writes an empty dest and reports changed: true (empty-to-empty is still a create).

MCP Setup Guide

Patchloom includes an MCP (Model Context Protocol) server for structured tool calls. MCP-capable AI agents can call Patchloom tools directly with JSON parameters, with no shell command construction, no quoting issues, and no --apply flag needed.

Official MCP Registry

Patchloom publishes to the official MCP Registry as io.github.patchloom/patchloom. The listing points clients at the crates.io and npm packages and starts the stdio server with mcp-server.

Repo metadata lives in server.json at the repository root. Ownership markers (required by the registry) are:

  • crates.io: a visible mcp-name: io.github.patchloom/patchloom line in the crate README (HTML comments are stripped on crates.io, so the token must be plain markdown text).
  • npm: mcpName in the published package.json (injected during the release publish-npm job; cargo-dist does not emit it).

On each release, after crates.io and npm publish succeed, the Release workflow calls Publish MCP Registry, which stamps the version into server.json, confirms those markers, authenticates with GitHub OIDC, and runs mcp-publisher publish. You can re-run it manually via workflow_dispatch with an explicit version (for example after a failed publish).

Smithery (local MCPB)

Smithery can list local stdio servers via an MCPB bundle (URL publish is for hosted Streamable HTTP endpoints; Patchloom's primary agent path is local stdio).

The bundle lives under mcpb/ and runs:

  1. patchloom mcp-server if the binary is on PATH, else
  2. npx -y patchloom@<version> mcp-server (Node.js 18+).
make pack-mcpb   # writes target/mcpb/patchloom-<version>.mcpb (override with VERSION=x.y.z)
# One-time auth + CI secret:
#   smithery auth login
#   export SMITHERY_API_KEY=$(smithery auth whoami --full | sed -n 's/^SMITHERY_API_KEY=//p')
#   gh secret set SMITHERY_API_KEY --repo patchloom/patchloom
bash scripts/publish-smithery.sh   # REST upload (reliable for stdio MCPB)

CI: .github/workflows/publish-smithery.yml packs after each GitHub Release and publishes when the SMITHERY_API_KEY secret is set (soft-skip otherwise).

Glama directory

Glama indexes open-source MCP servers for discovery, quality scores, and optional hosted connectors.

Listing (live): glama.ai/mcp/servers/patchloom/patchloom (search API id pk95432szu). Repo ownership metadata is in root glama.json (maintainers GitHub usernames).

Canonical short description (match server.json / MCP Registry, max 100 chars):

Agent-safe structured edits: JSON/YAML/TOML/md/AST, dry-run, batch/tx. Not a filesystem MCP.

If the Glama profile still shows the older “AI agent file editing via MCP…” blurb, open the listing as a maintainer and set the description to the text above (GitHub sync does not always replace an existing Glama description).

First-time submit (already done for Patchloom)

  1. Sign in at glama.ai (GitHub OAuth is supported).
  2. Open MCP Servers and click Add MCP Server.
  3. Submit GitHub URL https://github.com/patchloom/patchloom with the description above.
  4. Confirm search: GET https://glama.ai/api/mcp/v1/servers?query=patchloom returns a non-empty servers list.

There is no public unauthenticated submit API.

Verify MCP support

The MCP server is included by default in all pre-built binaries and in recommended installs (Homebrew, Scoop, npm npx / global, crates.io). winget usually tracks the latest GitHub Release after Microsoft publishes the version PR; Chocolatey often lags while versions wait for moderation. Prefer Scoop or a GitHub Release binary when you need a known-current MCP server. Verify it works:

patchloom mcp-server --help

Configure your agent

Set command to an absolute path to the binary, or to patchloom if the CLI is already on PATH (Homebrew, Scoop, cargo install, installer).

Examples:

PlatformTypical command value
Unix (Homebrew / cargo)patchloom or /opt/homebrew/bin/patchloom
Windows (Scoop)patchloom or C:\\Users\\you\\scoop\\shims\\patchloom.exe
Windows (portable zip)C:\\path\\to\\patchloom.exe

JSON configs need escaped backslashes on Windows paths (C:\\\\Users\\\\...\\\\patchloom.exe). Prefer the bare name patchloom when the shim is on PATH.

Grok (config.toml)

Add to ~/.grok/config.toml:

[mcp_servers.patchloom]
command = "patchloom"
args = ["mcp-server"]
env = { PATCHLOOM_MCP_SURFACE = "core" }

Claude Desktop (JSON)

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

VS Code (.vscode/mcp.json)

Create .vscode/mcp.json in your workspace root:

{
  "servers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Cursor (.cursor/mcp.json)

Create .cursor/mcp.json in your workspace root:

{
  "servers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Or use the Patchloom VS Code extension to configure MCP automatically via the Patchloom: Configure MCP command.

Generic stdio MCP

Any MCP client that supports stdio transport can connect by spawning patchloom mcp-server as a subprocess. The server communicates via JSON-RPC over stdin/stdout.

Available tools

Patchloom exposes two registration paths for MCP tools (see src/cmd/mcp/surface.rs for the inventory and policy):

PathRuleExamples
Registry (default)1:1 with a plan write Operation; schema from the variantdoc_set, create_file, fix_whitespace, most md_* writers
Custom (justified exception)Multi-file scan, multi-op/batch/plan, readonly query, AST analyze, patch/metasearch_files, replace_text, batch_*, execute_plan, doc_get / doc_query, all ast_*

Prefer the registry for new simple write tools. Do not force custom tools into the registry when that would lose multi-file, batch, or read UX.

Custom tools are inventoried in CUSTOM_MCP_TOOLS_CORE (always registered) and CUSTOM_MCP_TOOLS_AST (only when the ast feature is enabled). Default builds expose 58 tools (registry + custom). Builds without ast omit the AST tools so list_tools stays honest about what is callable.

ToolDescription
doc_setSet a value by selector path in a JSON, YAML, or TOML file
doc_deleteDelete a value by selector path from a structured file
doc_mergeDeep-merge an object into a document (optional selector, e.g. 0 for multi-doc YAML)
doc_appendAppend a value to an array
doc_prependPrepend a value to an array
doc_ensureSet a value only if the selector path does not exist
doc_delete_whereDelete array elements matching a predicate field (field=value)
doc_updateUpdate values at selector paths that use predicates or wildcards (items[name=a].v, items[*].enabled). Not a predicate field; doc_delete_where is the predicate tool.
doc_moveMove a value from one selector path to another
doc_getRead a value by selector path (read-only)
doc_queryQuery a structured file: has, keys, len, select, or flatten (read-only). Selector optional for keys/len (defaults to .).
doc_diffCompare two structured files (read-only)
search_filesSearch text files for a pattern, including literal, case-insensitive, count, file-only (files_with_matches / files_without_match), multiline, invert-match, and assert-count modes. Binary and invalid UTF-8 files are skipped (read-only)
list_filesBounded directory inventory with the same ignore/exclude/glob rules as search (read-only). Prefer this over a second generic filesystem MCP for list/tree. Optional max_depth prunes the walk per root (does not enter deeper dirs). max_results (default 500) still counts all in-depth matches then truncates and sets truncated / total_matched
git_statusShow uncommitted file changes vs git HEAD (read-only)
server_infoReturn server identity and workspace root: cwd, surface (full|core), tool_count, package version, and MCP protocol_version (read-only). Use cwd before relative path ops
read_fileRead file contents with optional line range
replace_textReplace text in a text file (literal or regex). Binary and invalid UTF-8 files are skipped
apply_fragmentFreeform fragment at a required after/before/old anchor; strips Morph-style // ... existing code ... markers (no cloud merge). Prefer when the agent has a snippet plus a known placement
md_upsert_bulletAdd a bullet under a markdown heading
md_table_appendAppend a row to a markdown table
md_replace_sectionReplace a section by heading (through next same-or-higher heading)
md_insert_after_headingInsert content immediately after a heading line (before body)
md_insert_after_sectionInsert content after the full section body (sibling section)
md_insert_before_headingInsert content before a markdown heading
md_move_sectionMove a heading section through next same-or-higher heading (same-file or cross-file)
md_dedupe_headingsRemove later whole sections with a duplicate heading (second body discarded)
md_lintLint an AGENTS.md file; returns {ok, path, issue_count, issues} (CLI lint-agents --json parity). Branch on ok; isError stays false when issues are present
fix_whitespaceFix whitespace and line endings in a text file. Binary and invalid UTF-8 files are skipped
create_fileCreate a new file with content
append_fileAppend content to an existing file
prepend_filePrepend content to an existing file
delete_fileDelete a file
move_fileMove or rename a file (binary-safe)
apply_patchApply a unified diff, Codex Begin Patch, or SEARCH/REPLACE / DiffFenced document (unique unless replace_all). Empty-hunk +++ /dev/null unlinks. A hunked delete applies minus first; leftover rewrites. Stale is ambiguous and does not unlink
batch_replaceReplace the same text across multiple files atomically
batch_tidyFix whitespace in multiple files atomically
execute_planExecute a full multi-op transaction plan atomically (recommended for complex/multi-file edits). Supports inline plan or plan_path. MCP strips format/validate; CLI tx still runs those lifecycle steps.
ast_listList symbol definitions (functions, classes, structs, enums, methods) in a file or directory (20 languages). Filter by kind.
ast_readRead a specific symbol's source code by name from a file.
ast_renameRename identifiers across files using AST-aware renaming (skips strings and comments).
ast_validateValidate syntax of source files. Returns parse errors with line numbers.
ast_searchStructural search using AST queries. Supports S-expression syntax and code patterns with meta-variables.
ast_refsFind all references to a symbol across files. Distinguishes definitions from references.
ast_depsExtract import/dependency statements from source files (Rust, Python, JS/TS, Go, Java, C/C++, Ruby, PHP).
ast_mapGenerate a ranked repository map using PageRank over the symbol reference graph. Token-budget-aware output.
ast_diffStructural diff between two versions of a file. Shows added, removed, and modified symbols.
ast_impactTransitive impact analysis: trace the reference graph to find all dependents of a symbol.
ast_replaceReplace text only within a specific symbol's body using AST scoping.
ast_insertInsert code before/after a symbol or inside a container (module, class, impl block).
ast_wrapWrap a symbol in a container (module, class, namespace, impl block, or custom wrapper).
ast_importsList, add, remove, or deduplicate import statements in source files.
ast_reorderReorder symbols by strategy: alphabetical, reverse, kind-first, or custom order.
ast_groupMove symbols into a new or existing module block within the same file.
ast_moveMove symbols between files with configurable insertion position. Optional update_imports plus old_module_path / new_module_path rewrites consumer imports.
ast_extract_to_fileExtract a symbol to a new file, optionally unwrapping module blocks. Optional update_imports plus old_module_path / new_module_path rewrites consumer imports.
ast_splitSplit a file by distributing symbols across multiple target files.

How MCP mode differs from CLI mode

AspectCLI modeMCP mode
Apply behaviorRequires --apply flagAuto-applies (writes are the default)
Input formatShell argumentsStructured JSON parameters
Path securityNo restrictionPaths must stay within working directory
Error formatstderr textMCP error response with structured content
DiscoveryAgent reads AGENTS.mdAgent discovers tools via MCP protocol
Lifecycle stepsCLI tx runs format/validateMCP execute_plan strips format/validate

Multi-step plans and concurrency guidance (important for agents)

For any work involving more than one edit (especially on the same file or related files), prefer the execute_plan tool over issuing many individual tools:

  • One execute_plan call = atomic execution of a mixed plan (doc.set + md.replace_section + create + replace + ...).
  • MCP execute_plan strips format and validate lifecycle steps from the submitted plan. Those steps do not run (they are CLI tx / project-config only). Use .patchloom.toml if the workspace needs lifecycle shells.
  • Top-level MCP strict overrides the plan only when the caller sends it. Omitting it leaves plan.strict unchanged (so {"plan":{"strict":false}} is honored).
  • write_policy on the plan is still accepted.

Example inline plan (JSON):

{
  "version": 1,
  "strict": true,
  "operations": [
    { "op": "doc.set", "path": "package.json", "selector": "version", "value": "2.0.0" },
    { "op": "md.replace_section", "path": "AGENTS.md", "heading": "## Commands", "content": "Run make check.\n" },
    { "op": "file.create", "path": "REPORT.md", "content": "# Summary\n" }
  ]
}

Critical rules for agents (to avoid lost updates and races):

  • Do not issue concurrent write tools against the same path(s) unless using execute_plan.
  • Serialize writes per path. Parallelize only across completely disjoint paths.
  • Per-call "ok" does not mean the combined result is coherent if you interleave writers yourself.
  • Use one execute_plan for any logical multi-edit task.

These semantics are also documented in the tool instructions returned by the MCP server and in patchloom agent-rules --mode mcp.

Tool surface (full vs core)

By default the server registers the full tool inventory (registry + custom handlers, including AST when built with ast). That default stays for compatibility.

Recommended for coding agents (Grok, Claude, Cursor, dual native tools + MCP): start with the core pack so tool schemas stay small. Upgrade to full when you need AST or advanced standalone tools.

export PATCHLOOM_MCP_SURFACE=core
patchloom mcp-server
ValueEffect
unset or fullFull inventory (default; backward compatible)
coreOnly: read_file, search_files, list_files, replace_text, batch_replace, doc_get, doc_set, doc_query, md_replace_section, execute_plan, server_info
anything elseServer fails to start with a clear error

server_info includes "surface": "core"|"full", "tool_count", package "version", MCP "protocol_version" (same as the initialize handshake), and when surface is full a "recommendation" string pointing coding agents at core. Invalid PATCHLOOM_MCP_SURFACE values are rejected (do not silently fall back).

Handshake instructions match the active surface: core mode lists only the core tools (it does not advertise full-inventory names such as create_file or ast_*). Instructions also include a short canonical name map (CLI / plan / MCP) and an explore rule (prefer list_files / search_files / read_file over shell cat/find/ls and over a second generic filesystem MCP when Patchloom MCP is connected).

Note: execute_plan remains on core, so multi-op plans can still run create/delete/AST-style plan ops (and plan apply.fragment) when the host sends a full plan. Standalone tools such as apply_fragment, create_file, and ast_* are full-inventory only. The env flag reduces the tool schema surface for small agents; it is not a capability sandbox for the plan catalog.

Short system-prompt rules (not the full ~50KB dump):

patchloom agent-rules --surface core

Coding-agent install recipes (core by default)

Coding agents should use the core pack so tool schemas stay small. Full inventory remains the product default when the env is unset (compatibility). Set PATCHLOOM_MCP_SURFACE=core in the client config for Grok, Claude, Cursor, and Codex-style hosts.

Grok / config.toml

[mcp_servers.patchloom]
command = "patchloom"
args = ["mcp-server"]
env = { PATCHLOOM_MCP_SURFACE = "core" }

Cursor (~/.cursor/mcp.json or project .cursor/mcp.json)

{
  "mcpServers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Claude Desktop / Claude Code style MCP config

{
  "mcpServers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Codex / generic stdio MCP

{
  "name": "patchloom",
  "command": "patchloom",
  "args": ["mcp-server"],
  "env": {
    "PATCHLOOM_MCP_SURFACE": "core"
  }
}

Do not also install a generic filesystem MCP only for list/read when Patchloom MCP is connected: use list_files, search_files, and read_file instead. Use full surface (unset env or full) when you need standalone AST tools or advanced md/doc tools as first-class MCP tools (execute_plan on core can still run plan ops).

Example fragment for copy-paste: mcp-core.example.json.

Design notes: mcp-surface-tiers.md.

Debugging and logging

The MCP server can log every tool call to a JSONL file for debugging and performance analysis. Each line records the tool name, duration, and success/failure status.

Enable logging with the --log flag:

patchloom mcp-server --log /tmp/patchloom-mcp.log

Or set the PATCHLOOM_MCP_LOG environment variable (the --log flag takes precedence):

export PATCHLOOM_MCP_LOG=/tmp/patchloom-mcp.log
patchloom mcp-server

Each line is a JSON object:

{"ts":1749123456789,"tool":"replace_text","duration_ms":3,"ok":true}
{"ts":1749123456800,"tool":"doc_set","duration_ms":5,"ok":false,"error":"file not found"}
FieldTypeDescription
tsnumberUnix timestamp in milliseconds
toolstringTool name that was called
duration_msnumberExecution time in milliseconds
okbooleanWhether the call succeeded
errorstringError message (only present on failure)

Configuring logging in your MCP client

Grok (config.toml) -- pass the env var to the MCP server subprocess:

[mcp_servers.patchloom]
command = "/path/to/patchloom"
args = ["mcp-server"]
env = { PATCHLOOM_MCP_LOG = "/tmp/patchloom-mcp.log" }

Or use --log in the args:

[mcp_servers.patchloom]
command = "/path/to/patchloom"
args = ["mcp-server", "--log", "/tmp/patchloom-mcp.log"]

Claude Desktop / VS Code / Cursor (JSON) -- use --log in the args:

{
  "mcpServers": {
    "patchloom": {
      "command": "/path/to/patchloom",
      "args": ["mcp-server", "--log", "/tmp/patchloom-mcp.log"]
    }
  }
}

Or pass the env var (Claude Desktop supports env in server config):

{
  "mcpServers": {
    "patchloom": {
      "command": "/path/to/patchloom",
      "args": ["mcp-server"],
      "env": { "PATCHLOOM_MCP_LOG": "/tmp/patchloom-mcp.log" }
    }
  }
}

Security model

The MCP server enforces path containment: every path must resolve inside the working directory where patchloom mcp-server was started. Relative paths are preferred. Absolute paths that resolve inside that workspace are allowed (agents often concatenate server_info.cwd with a relative path). ../ traversal, absolute paths outside the workspace, and symlinks that escape the workspace are rejected. This prevents an agent from editing files outside the project.

Each individual tool validates every path before execution.

Streamable HTTP transport

By default, the MCP server uses stdio transport (ideal for local IDE/agent integration). With --http, the server switches to Streamable HTTP transport, allowing remote MCP clients to connect over the network.

Streamable HTTP has no authentication and no token. The default bind is loopback (127.0.0.1). Binding a non-loopback address (0.0.0.0, a LAN IP, or a public IP) is refused unless you pass --allow-unauthenticated.

Basic HTTP

# Default: listen on 127.0.0.1:8080 (loopback)
patchloom mcp-server --http

# Custom port on loopback
patchloom mcp-server --http --port 3000

# Explicit loopback bind
patchloom mcp-server --http --host 127.0.0.1

The MCP endpoint is served at /mcp (e.g., http://127.0.0.1:8080/mcp).

Do not copy-paste --host 0.0.0.0 as a default. All-interfaces HTTP is unauthenticated (there is no token) and requires an explicit opt-in:

# All interfaces: unauthenticated, opt-in required
patchloom mcp-server --http --host 0.0.0.0 --allow-unauthenticated

HTTPS with native TLS

TLS encrypts the connection but still does not authenticate clients. Non-loopback binds still need --allow-unauthenticated. There is no bearer token.

patchloom mcp-server --http --host 0.0.0.0 --port 443 \
  --tls-cert cert.pem --tls-key key.pem --allow-unauthenticated

Both --tls-cert and --tls-key must be provided together. The server uses rustls (no OpenSSL dependency).

HTTP transport flags

FlagDefaultDescription
--httpoffUse Streamable HTTP transport instead of stdio
--host127.0.0.1Bind address (requires --http). Non-loopback binds require --allow-unauthenticated
--port8080Bind port (requires --http). Use 0 for an OS-assigned ephemeral port (printed in the startup banner)
--allow-unauthenticatedoffPermit HTTP on a non-loopback bind. Streamable HTTP has no token or other client auth
--tls-certnoneTLS certificate PEM file; enables HTTPS (requires --http and --tls-key)
--tls-keynoneTLS private key PEM file (requires --http and --tls-cert)

Connecting a remote MCP client

Use any MCP client that supports Streamable HTTP transport. Example with the rmcp Rust client:

#![allow(unused)]
fn main() {
// Requires `rmcp` 3.x with client + HTTP transport features (see crates.io).
use rmcp::ServiceExt;
use rmcp::transport::StreamableHttpClientTransport;

let transport = StreamableHttpClientTransport::from_uri("http://localhost:8080/mcp");
let client = ().serve(transport).await?;
// Negotiate initialize; tool calls go through client.peer().call_tool(...).
}

Logging with HTTP transport

The --log flag works identically with HTTP transport:

patchloom mcp-server --http --log /tmp/mcp.log

Graceful shutdown

The HTTP server shuts down gracefully on Ctrl+C (SIGINT): active SSE streams are terminated, in-flight requests complete, and the server exits cleanly.

Example tool call

An MCP-capable agent sends:

{
  "method": "tools/call",
  "params": {
    "name": "doc_set",
    "arguments": {
      "path": "config.yaml",
      "selector": "database.port",
      "value": 5432
    }
  }
}

Patchloom parses the YAML, changes database.port to 5432, preserves all comments and formatting, and writes the file. The agent receives a success response with no further action needed.

Editor Extension

Patchloom has a companion editor extension that handles binary discovery, AGENTS.md generation, MCP server configuration, and structured file operations from the command palette. It works in VS Code, Cursor, Windsurf, and VSCodium.

Install

Install from either registry:

Or search for Patchloom in the Extensions view (Ctrl+Shift+X / Cmd+Shift+X).

What the extension does

One-click workspace setup

Run Patchloom: Setup Workspace from the command palette. It walks through binary detection, AGENTS.md generation, and MCP server configuration in one pass. If the CLI is not installed, you can install it directly from the command palette with Patchloom: Install Patchloom.

MCP server configuration

Patchloom: Configure MCP injects the Patchloom MCP server into your editor's config file. Supports:

  • VS Code (.vscode/mcp.json)
  • Cursor (.cursor/mcp.json)
  • Windsurf (~/.codeium/windsurf/mcp_config.json)

This replaces the manual JSON editing described in the MCP Setup guide.

Agent rules generation

Patchloom: Initialize Project generates an AGENTS.md file from patchloom agent-rules. If one already exists, the extension opens a diff so you can merge updates manually.

Quick actions

Patchloom: Quick Action opens an interactive picker with structured editing operations:

ActionWhat it does
Replace textLiteral text replacement with diff preview
Tidy fileWhitespace and newline cleanup with diff preview
Set structured valueUpdate a JSON, YAML, or TOML value by selector path with diff preview
Search textFind pattern matches across workspace files
Create fileScaffold a new file and open it in the editor
Read structured valueRead a JSON/YAML/TOML value by selector path and copy to clipboard
Merge patch (three-way)Apply a stale patch using three-way merge

Batch operations

Patchloom: Batch Apply opens a line-oriented plan template where you compose multiple operations (replace, tidy, doc set). The extension pipes the plan to patchloom batch --apply so all changes land atomically.

Status bar

The status bar shows MCP and binary readiness at a glance. Click it for full diagnostics, including per-editor MCP configuration status.

Verify MCP Server

Patchloom: Verify MCP Server spawns the MCP server, sends a JSON-RPC initialize handshake, and confirms the server responds correctly.

When to use the extension vs the CLI

The extension automates the setup steps described in the Quickstart: installing the binary, running patchloom init, and configuring MCP. If you use VS Code, Cursor, Windsurf, or VSCodium, the extension is the fastest way to get started.

The CLI remains necessary for CI scripts, non-editor agents, and environments where a VS Code extension is not available.

Source and issues

Patchloom Reference

This is the reference for Patchloom's meaningful commands, actions, operations, and notable command modes.

  • Start with Quickstart if you want a first success.
  • Read Core Concepts for shared semantics like write modes, exit codes, and transaction behavior.
  • Use this file when you need to choose the right feature or mode for a job, or when a pull request adds meaningful CLI surface and the docs coverage test expects it here.

Global behaviors

Patchloom has a small set of global features that shape how other commands behave.

Write modes

Patchloom write commands default to preview mode. The canonical semantics live in Core Concepts. The sections below focus on when to choose each mode.

--diff

  • What it does: Prints the unified diff for a write command without mutating files.
  • Use when: You want a human review step before applying a change, or you want to inspect the exact patch Patchloom would write.
  • Prefer instead: Use --check for CI pass or fail behavior, or --apply to actually write files.

--apply

  • What it does: Writes the requested change to disk.
  • Use when: You have already previewed the change, or you trust the command and want the mutation to happen now.
  • Prefer instead: Use --diff when reviewing, or --check when you only need a clean or dirty signal.
  • Not from project config: [defaults] apply in .patchloom.toml is ignored. Write mode is --apply / --check / --diff / --confirm only.

--check

  • What it does: Calculates whether a write command would change files and returns exit code 2 when changes are pending.
  • Use when: You are wiring Patchloom into CI, pre-commit validation, or agent workflows that should fail on drift.
  • Prefer instead: Use --diff when you need the actual patch text, or --apply when you want the mutation.

--confirm

  • What it does: Shows the diff preview, then prompts Apply? [Y/n] on stderr. If confirmed, applies the change; if declined, exits without writing.
  • Use when: You want a single-command preview-then-apply workflow instead of running the command twice.
  • Prefer instead: Use --apply when scripting (no interactive prompt), or --diff when you only want the preview.

--apply and --check conflict with each other (and with --confirm). --diff is not exclusive: you can pass --diff --apply to print a unified diff after a successful write. When none of --apply, --check, or interactive confirm is used, the default is preview (show what would change without writing; exit 2 when changes would occur). When --confirm is used and stdin is not a TTY, the command shows the preview without prompting.

Write policy flags

These flags shape how written content is normalized before it reaches disk.

--ensure-final-newline

  • What it does: Ensures non-empty written files end with \n.
  • Use when: You want simple newline hygiene on every touched file without running a separate cleanup command.
  • Prefer instead: Use tidy fix when the goal is repo cleanup, not just normalization of files already being edited.

--normalize-eol

  • What it does: Normalizes written line endings to keep, lf, crlf, or cr.
  • Use when: A repo or downstream tool expects a specific line ending convention.
  • Prefer instead: Use --respect-editorconfig when the repo already declares the desired convention there.

--trim-trailing-whitespace

  • What it does: Removes trailing spaces and tabs from touched lines before writing.
  • Use when: You want text cleanup to happen automatically as part of another write command.
  • Prefer instead: Use tidy fix when the goal is to sweep existing files for whitespace problems.

--respect-editorconfig

  • What it does: Reads .editorconfig when present and applies matching write policy (insert_final_newline, end_of_line, trim_trailing_whitespace, and charset). charset = utf-8-bom ensures a leading UTF-8 BOM; charset = utf-8 does not insert one (and strips a leading BOM). utf-16le / utf-16be are invalid_input.
  • Use when: The repo already encodes formatting policy in .editorconfig and Patchloom should follow it automatically.
  • Prefer instead: Use explicit write flags, or tx write_policy, when the command should be self-contained and not depend on repo metadata.

--collapse-blanks

  • What it does: Collapses consecutive blank lines into a single blank line after writing. Useful after line deletion to prevent double-blank gaps.
  • Use when: You are deleting lines (e.g. with replace --whole-line --new '') and want to clean up the resulting blank line runs.
  • Prefer instead: Omit when consecutive blank lines are intentional (e.g. section separators in code).

--format

  • What it does: Runs a shell command after every successful --apply write. Intended for formatters (e.g. prettier --write ., cargo fmt).
  • Use when: The repo has an autoformatter and you want Patchloom to invoke it after each mutation so files stay formatted.
  • Prefer instead: Omit when the formatter is already run separately, or when using --diff/--check modes (the command only fires on --apply).
  • Failure behavior: Non-zero exit or timeout exits 1 with error_kind: "format_failed" under --json/--jsonl. The write may already be on disk; JSON includes backup_session when a session was created, plus applied: true (canonical; #1831), write_applied: true (deprecated alias), files_changed, and files[].path for written paths (#1795). Use undo or re-run the formatter.
  • Containment: Under --contain, the command (including [defaults] format / [format] command) is scanned with the same shell-metacharacter refuse as plan lifecycle. Pipelines, ;, redirects, and substitutions are guard_rejected and are not executed. Plain formatters such as cargo fmt still run. Without --contain, trusted local format is unchanged.

--format-timeout

  • What it does: Sets the maximum time in seconds the --format command is allowed to run before being killed. Defaults to 30 seconds.
  • Use when: The formatter is slow (e.g. large monorepo) and the default 30 second timeout is insufficient.
  • Prefer instead: Keep the default unless the formatter demonstrably needs more time.
  • Failure behavior: Exceeding the timeout kills the formatter process tree and exits 1 with error_kind: "format_failed" (same envelope as a failing --format command).

--no-format

  • What it does: Disables post-write formatting even if configured in .patchloom.toml (via [format] auto = true or [defaults] format).
  • Use when: You want to skip formatting for a single invocation without changing the project config.
  • Prefer instead: Omit when you want the configured formatter to run normally.

Output and scope flags

These flags affect how Patchloom reports results or chooses which files to touch.

--json

  • What it does: Emits one machine readable JSON document for the command result.
  • Use when: Another tool, script, or agent needs structured output instead of human oriented text.
  • Prefer instead: Use --jsonl when you want one JSON object per result line for streaming style consumers.

--jsonl

  • What it does: Emits one compact JSON value per result line instead of one aggregate document.
  • Use when: A command naturally yields multiple result records, or you want compact machine-readable output from single-result commands like create, delete, rename, status, tx, explain, or undo.
  • Prefer instead: Use --json when you want one aggregate document for the whole command.

--quiet

  • What it does: Suppresses non-JSON human readable output.
  • Use when: Only the exit code or the file mutation matters and extra stdout noise would get in the way.
  • Prefer instead: Use --json when another tool still needs structured output.

--cwd

  • What it does: Sets the working directory used to resolve relative paths for operation targets and meta-input files: tx plan files, batch ops files, patch patch files, explain plan files, and --files-from list files (absolute meta paths are unchanged).
  • Use when: You are invoking Patchloom from outside the target repo, or you want scripts to behave predictably regardless of the caller's current directory. Example: patchloom --cwd /repo batch ops.txt finds /repo/ops.txt.
  • Prefer instead: Use a plan level cwd in tx when the directory choice should travel with the plan itself, but keep it inside the invocation root. Relative plan cwd values resolve from the caller's working directory (--cwd or the process cwd), not from the plan file location.
  • Not a sandbox: Without --contain, paths may escape via ../ or absolute paths. MCP always enforces containment; use --contain for the same on CLI. On Windows, drive-relative dests (C:foo) and root-relative dests (\foo) are invalid_input: they ignore --cwd (process current directory on that drive, or the drive root).

--contain

  • What it does: Rejects file paths that escape the working directory (via ../, absolute paths that resolve outside the workspace, or symlinks that resolve outside the workspace). Uses PathGuard with AllowIfContained: absolute paths that canonicalize under --cwd (or the process cwd) are allowed so agents may pass absolutized paths. Mirrors library-style containment for reads and writes: explicit paths on search / read / replace / create / delete / rename / append / prepend / patch / md / doc / tidy / ast (list, read, rename, validate, search, refs, deps, map, impact, diff, replace) / status / tx / batch (including binary/case-only rename). Also applies to meta-input files: tx/explain plan files, batch ops files, patch patch files, and --files-from list files (so a list path cannot open /etc/passwd or ../outside.txt under --contain). MCP tool path arguments use the same AllowIfContained rule (always on); prefer relative paths.
  • Use when: An agent or automation should not be able to read or write outside --cwd (or the process cwd). Pair with --cwd for a workspace root.
  • Default: Off. CLI remains unrestricted for human scripts (same trust model as make / sh).
  • Prefer instead: Use the MCP server when the agent already has MCP tools; containment is always on there (in-workspace absolute paths allowed; outside-workspace and ../ rejected).
  • Format hooks: Config and --format shell commands are run through the same metacharacter refuse as plan format/validate (refuse_lifecycle_shell_metas). curl|sh and ; are not executed.

--glob

  • What it does: Restricts candidate files by one or more glob patterns. Patterns match either the basename or the path relative to the input root, so sub/*.txt matches files under a searched sub/ directory.
  • Use when: A command should only see a narrow file type or subtree, even if the input path is broader.
  • Prefer instead: Use --files-from when another tool has already determined the exact file list.
FormScopeUse
dest *.txtCurrent directory onlyCLI search / replace / tidy dest (Windows/PowerShell pass it through; Unix shells expand it before exec)
dest **/*.txtRecursive dest globNested files as a search/replace/tidy dest
--glob '*.txt'Filter after walking dest rootsNested files under . or src/
plan pathOne file or directoryNever *.txt. Use replace "glob": "*.txt" or plan-level for_each.glob

--exclude

  • What it does: Excludes paths matching the given glob patterns (applied after .gitignore and any custom ignore files). May be repeated. Complements --glob.
  • Use when: You want to layer additional excludes (e.g. target/** or build artifacts) on top of custom ignore files / .gitignore for search, replace, or tidy.
  • Parity: Matches SearchOptions.exclude_patterns and the library collect_file_paths_with_ignores precedence.

--ignore-file

  • What it does: Specifies additional gitignore-style ignore files (e.g. .agentignore, .cursorignore) to respect during file collection. May be repeated.
  • Use when: LLM agents or projects use tool-specific ignore files and want CLI / tx / MCP search (and replace/tidy) to honor the same layered ignores as the pure-library API.
  • Parity: Matches SearchOptions.custom_ignore_filenames.

--files-from

  • What it does: Reads the target file list from a file, or from stdin when passed -. A relative list path is resolved under --cwd when that flag is set (absolute list paths are unchanged). Paths inside the list are still resolved against --cwd / the process cwd as usual, and under --contain each listed path must stay in the workspace.
  • Exact paths only: Each line is one file path. Directory entries are skipped (not walked). Blank lines are ignored. Lines whose first non-whitespace character is # are comments and are ignored (#1811; gitignore-style). Paths that literally start with # are not supported.
  • No walk fallback: An empty list (or only blanks) searches/replaces zero files. Patchloom does not fall back to walking .. Search/replace "no matches" messages name --files-from … rather than bare ..
  • Failure behavior: A missing list file is not_found. Invalid UTF-8 (including a UTF-16 BOM) and a UTF-16 list without a BOM (NUL bytes that are still valid UTF-8) are invalid_input, not not_found of the listed names. On Windows, a drive-relative (C:ok.txt), root-relative (\ok.txt), trailing-dot, or ADS (ok.txt:Zone.Identifier) line is invalid_input of that dest (same peel as create); the list file is not named as missing. When every listed dest is a missing relative path (nope.txt), search/replace still report all-missing not_found of the --files-from list scope, not of each dest.
  • Use when: Another tool already selected the exact paths and Patchloom should operate only on that set.
  • Prefer instead: Use --glob for pattern based scoping, or direct path arguments when the target set is already small and obvious.

--color

  • What it does: Controls when ANSI color codes appear in output. auto (default) enables color when stdout is a terminal and the NO_COLOR environment variable is not set. always forces color even when piped. never disables color unconditionally.
  • Use when: You need to override the default terminal detection, for example forcing color into a pager or disabling it in a terminal that renders escape codes literally.
  • Prefer instead: Set the NO_COLOR environment variable when you want a global, tool-agnostic way to disable color across all CLI tools.

format_config (internal)

  • What it does: Carries the per-extension formatter configuration loaded from .patchloom.toml ([format.by_extension] table) so that post-write formatting can run the correct formatter for each file type.
  • Use when: You configure [format] auto = true and [format.by_extension] in .patchloom.toml. The field is populated automatically; it is not set via CLI flags.

--verbose

  • What it does: Prints diagnostic messages to stderr prefixed with [patchloom]. Shows which operations are running, search parameters, selector path evaluation steps, and MCP tool call timing. Can also be enabled by setting the PATCHLOOM_LOG environment variable to any value.
  • Use when: A command produces unexpected results and you need to see what Patchloom is doing internally without reading source code.
  • Prefer instead: Use --json when you need machine-readable output for downstream tools.

Exit codes

Use Core Concepts as the canonical exit code table. When integrating Patchloom into CI or agent workflows, branch on exit codes instead of parsing human readable output.

Commands

These are the main entry points. If you are deciding between commands, start here.

  • What it does: Searches text files with literal or regex matching, optional context, counts, and file only results. Binary and invalid UTF-8 files are skipped.
  • Use when: You need to locate candidate edits, audit repo state, or narrow inputs before changing files. For AI agents, native search/grep tools are typically faster for simple pattern matching.
  • Prefer instead: Use replace for actual text mutation, or doc, md, or patch when you already know the structured change you want.
  • Failure behavior: Pattern miss on existing roots exits 3 with error_kind: "no_matches". When every explicit path root (or non-stdin --files-from entry) is missing, exit 1 with error_kind: "not_found". Empty pattern, incompatible flags (-l with -L, --unique, invalid regex) use invalid_input. Search --unique is rejected with a pointer at replace --unique (search is read-only).
  • Related: --glob, --files-from, replace

replace

  • What it does: Performs mechanical string replacement across one or many text files, with literal or regex matching. Binary and invalid UTF-8 files are skipped. Regex ^ and $ use the same line ends as search (LF, CRLF, and a lone CR), including mid-file CR.
  • Use when: You are doing a rename, version bump, boilerplate rewrite, or another string level change where plain text semantics are enough. For AI agents doing single-file replacements, native search_replace tools are typically faster; use patchloom replace inside tx plans when batching multiple file edits.
  • Prefer instead: Use doc for structured data, md for heading aware markdown, or patch when you already have a unified diff.
  • Failure behavior: Soft pattern miss exits 3 with error_kind: "no_matches"; --unique multi-match exits 5 with ambiguous. All-explicit-path-missing (or all-missing --files-from list) exits 1 with not_found. Empty --files-from (empty list file or empty stdin) exits 1 with invalid_input (not pattern miss; #1796). Validation failures and invalid regex patterns use invalid_input.
  • Multi-path honesty: Explicit multi-file lists report zero-match paths under refused[] with reason: no_matches while applying matches on other paths (#1792). Missing explicit paths soft-skip under skipped[] on CLI (partial apply); MCP batch_replace hard-fails missing paths and rolls back (#1793). Under --json/--jsonl, missing-path stderr lines are suppressed when paths are already in skipped[] (#1797).
  • Related: search, tx, apply-fragment

apply-fragment

  • What it does: Applies a freeform text fragment with a required placement anchor. Strips Morph-style lazy marker lines such as // ... existing code ... from --fragment, then inserts after/before an anchor or replaces a unique old span. Dry-run by default. Morph here means MorphLLM Fast Apply (a cloud apply product); Patchloom only reuses the common marker style and does not call Morph's API.
  • Use when: An agent emitted a Morph-class lazy snippet but you know a unique placement (after/before/old). Prefer this over guessing without anchors.
  • Flags: Exactly one of --after, --before, or --old. Provide text via --fragment or --stdin. Default uniqueness: fail when the anchor matches more than once. Pass --allow-non-unique only when multi-match is intentional (plan/MCP field is unique, default true). A fragment without a trailing newline is still placed on its own line next to a whole-line or indented whole-line anchor; the anchor's indent is copied (including when the --after/--before text itself includes those leading spaces). A trailing newline on --fragment after a whole-line --after does not insert a blank line.
  • Prefer instead: replace for exact known spans; ast replace / ast rename for code structure; never a cloud Morph merge for offline deterministic hosts.
  • Failure behavior: Missing placement or empty fragment after strip → invalid_input (exit 1). Anchor miss → no_matches (exit 3). Multi-match with default unique → ambiguous (exit 5). Preview without --apply → exit 2 when changes would apply.
  • Related: replace, tx plan op apply.fragment, MCP apply_fragment, docs/plans/morph-gap-matrix.md

patch

  • What it does: Checks or applies a unified diff, a Codex *** Begin Patch document (Add / Update / Delete / Move), or an Aider SEARCH/REPLACE / DiffFenced document.
  • Use when: The change already exists as a patch, Begin Patch envelope, or SEARCH/REPLACE document, or you want stale context detection instead of search and replace semantics.
  • Paths: A relative patch file path is resolved under --cwd. Paths inside the unified diff are also resolved against --cwd. Git dest prefixes a/ and b/ also accept a\ and b\.
  • Prefer instead: Use replace, doc, or md when you want to describe the mutation directly instead of carrying a diff artifact.
  • Related: patch check, patch apply, patch merge, tx patch.apply

md

  • What it does: Performs heading aware markdown edits for sections, bullets, tables, and AGENTS linting.
  • Use when: Documentation needs semantic markdown edits that should not depend on raw byte offsets.
  • Prefer instead: Use replace for simple line level edits, or patch for exact diff application.
  • Related: md actions, tx markdown operations

doc

  • What it does: Performs parser backed JSON, YAML, and TOML queries and mutations.
  • Use when: Config or metadata changes should operate on keys and arrays instead of brittle text matching.
  • Prefer instead: Use replace for plain text, md for markdown, or patch for existing diffs.
  • Related: doc actions, tx document operations

tidy

  • What it does: Checks or fixes trailing whitespace, line endings, and final newlines in text files. Binary and invalid UTF-8 files are skipped.
  • Use when: You need repo text normalization, or a CI guard for basic text tidiness.
  • Failure behavior: tidy fix with both --dedent and --indent exits 1 with error_kind: "invalid_input" under --json/--jsonl. When every explicit path root is missing, exit 1 with error_kind: "not_found" (not vacuous clean success). Pending tidy issues under tidy check exit 2 (CHANGES_DETECTED).
  • Prefer instead: Use write policy flags when the cleanup should only apply to files already being touched by another command.
  • Related: tidy check, tidy fix, tx tidy.fix

append

  • What it does: Appends content to the end of an existing file. If the file does not end with a newline, one is inserted before the appended content. Exactly one of --content or --stdin is required. Fails if the file does not exist (unlike create).
  • Use when: Adding tests, changelog entries, rules, or any content to the end of a file without reading the entire file to find a unique anchor.
  • Failure behavior: Missing file exits 1 with error_kind: "not_found"; bad flags or directory targets use invalid_input (JSON envelopes).
  • Prefer instead: Use replace when the insertion point is not the end of the file.
  • Related: create, prepend, tx file.append

prepend

  • What it does: Prepends content to the beginning of an existing file. Exactly one of --content or --stdin is required. Fails if the file does not exist or if the target is a directory.
  • Use when: Adding headers, copyright notices, shebang lines, or any content to the beginning of a file without reading the entire file to find a unique anchor.
  • Failure behavior: Same as append: not_found / invalid_input under --json/--jsonl.
  • Prefer instead: Use replace when the insertion point is not the beginning of the file.
  • Related: append, create, tx file.prepend

create

  • What it does: Creates a file from literal content or stdin. Exactly one of --content or --stdin is required. Passing both is rejected with --content and --stdin cannot be combined, and passing neither is rejected with either --content or --stdin must be provided. Directory targets are rejected in all modes. When combined with --confirm and --json or --jsonl, the structured output includes applied: true|false so callers can tell whether the prompt was accepted.
  • Use when: Generating a new tracked file is the whole task, or one step in a larger transaction. For AI agents creating a single file, native file creation tools are typically faster; use file.create inside tx plans when bundling with other edits.
  • Failure behavior: Existing file without --force exits 1 with error_kind: "already_exists"; bad flags/non-file targets use invalid_input. On Windows, a trailing \ or / on the dest is the same dest without those slashes (keep.txt\\ is keep.txt), so an existing file is already_exists and is not unlinked. Pre-write failures under --json/--jsonl set applied: false.
  • Prefer instead: Use doc, md, or replace when the file already exists and only needs edits.
  • Related: delete, tx file.create

delete

  • What it does: Removes a file, symlink, FIFO, socket, or device node. Directory targets are rejected in all modes. Symlinks are unlinked without following the target (including dangling links and symlink-to-dir). When combined with --confirm and --json or --jsonl, the structured output includes applied: true|false so callers can tell whether the prompt was accepted.
  • Use when: A file should disappear outright and no other atomic edits are needed. For AI agents deleting a single file, native delete tools are typically faster; use file.delete inside tx plans when bundling with other edits.
  • Failure behavior: Missing file exits 1 with error_kind: "not_found"; directory targets use invalid_input. Pre-write failures under --json/--jsonl set applied: false.
  • Prefer instead: Use tx file.delete when the removal must be bundled atomically with other changes.
  • Related: create, tx file.delete

rename

  • What it does: Moves (renames) a file, symlink, FIFO, socket, or device node from one path to another. Real directories are refused. Symlinks (including dangling and symlink-to-dir) are moved as directory entries without following the target, so write policies never rewrite the link target. When combined with --confirm and --json or --jsonl, the structured output includes applied: true|false so callers can tell whether the prompt was accepted.
  • Use when: A file needs to be relocated and no other atomic edits are needed. Use file.rename inside tx plans when bundling with other edits.
  • Failure behavior: Missing source exits 1 with error_kind: "not_found"; destination exists without --force uses already_exists; real directories use invalid_input. Pre-write failures under --json/--jsonl set applied: false.
  • Prefer instead: Use tx file.rename when the rename must be bundled atomically with other changes.
  • Related: create, delete, tx file.rename

tx

  • What it does: Runs multiple operations atomically, then optional format and validate steps.
  • Use when: Editing 3 or more files in one task. Batches N operations into 1 tool call, eliminating agent round-trips. Also provides atomicity, rollback, and format/validate lifecycle. For AI agents, this is the primary speed advantage: one call instead of N.
  • JSON status: Preview (default), --diff, and --check with pending changes report status: "changes_detected" and exit 2. Applied success reports status: "success" and exit 0. Do not treat ok: true alone as "applied."
  • Prefer instead: Use standalone commands when one direct operation is enough.
  • Related: examples, tx fields, tx operations

batch

  • What it does: Executes multiple operations from a simple line-oriented format. Each line is one operation with positional arguments (e.g., doc.set config.json version "2.0.0"). Internally builds a tx plan and delegates to the tx engine.
  • Use when: Editing multiple files and the JSON tx plan format is too verbose. The line format covers 28 operations (doc.set, doc.delete, doc.merge, doc.ensure, doc.append, doc.prepend, doc.update, doc.move, doc.delete_where, replace with optional flags, file.append, file.prepend, file.create, file.delete, file.rename, md.upsert_bullet, md.table_append, md.replace_section, md.insert_after_heading, md.insert_after_section, md.insert_before_heading, md.move_section, md.dedupe_headings, md.lint_agents, tidy.fix, ast.rename, ast.replace, ast.rewrite_signature) with minimal syntax. For AI agents, this is faster to generate than a full JSON plan.
  • Paths: A relative ops file path is resolved under --cwd (same as tx plan files). Paths inside ops lines are also resolved against --cwd.
  • Replace order: Batch replace is replace PATH OLD NEW (not CLI replace OLD --new NEW path). CLI flag --new (and plan-shaped --from/--to) is rejected with a PATH OLD NEW hint; path-last positionals also fail with a parse hint when the third token is an existing file. Bare old=/new= (and from=/to=) prefixes on those tokens are peeled.
  • --if-exists: Optional on replace, doc.set, and file.delete. Soft-skips a missing file (and, for doc.set, a missing selector) so sibling lines still apply.
  • Plan-shaped keys: Optional key=value prefixes on positionals are peeled so pasted plan/MCP keys do not become file bytes: file content=/body=/path=, md heading=/content=/bullet=/row= plus md.move_section/md.dedupe_headings/md.lint_agents path=/before=/after=, file.rename from=/to=, ast.rename/ast.replace/ast.rewrite_signature old=/new=/symbol=/parameters=/return_type=, doc selector=/key=/value=/predicate=, and tidy.fix path=.
  • Quoting: Double-quoted tokens allow only \" and \\. Sequences like \n are literal (not newlines). Prefer tx / MCP JSON for multi-line content, or put real newlines outside one-line quoted strings.
  • Values (doc.set and friends): After quote removal, each value token is parsed as JSON. Batch doc.set f.json v "2.0" still stores a number because the token text is 2.0. Force a string with nested JSON quotes: doc.set f.json v "\"2.0\"", or use tx / MCP with "value": "2.0". Same rule as CLI doc set (see agent-rules note).
  • Failure behavior: Line parse failures (unknown op, bad arity, bad quotes, CLI-order replace) exit 4 (PARSE_ERROR) with error_kind: "parse_error" and applied: false under --json/--jsonl. Too many operations (over the hard cap) exits 1 with invalid_input. Runtime op failures use the shared tx exit codes. Preview with changes uses the same status: "changes_detected" / exit 2 contract as tx.
  • Prefer instead: Use tx when you need format/validate lifecycle steps, strict mode, multi-line content, or operations not supported by the line format (patch.apply, replace with regex/nth, search, read).
  • Related: tx

read

  • What it does: Prints the contents of one or more files, optionally restricted to a line range. --lines uses the same numbering as search (LF, CRLF, and a lone CR). Selected slices are joined with LF. Multiple files get ==> path <== separators in text mode, a JSON array in --json mode, and one object per line in --jsonl mode. If at least one requested file is read successfully, the command still exits successfully and reports errors only for the missing files.
  • Use when: An agent needs to inspect one or several files before deciding on an edit. For AI agents, native read_file tools are typically faster for single-file reads.
  • Failure behavior: Invalid --lines exits 1 with error_kind: "invalid_input". When every path fails, exit 1 with error_kind: "not_found". Partial success still exits 1 after emitting successful reads.
  • Prefer instead: Use search when you need pattern matching, or doc get when the file is structured and you want a single value.

Library API

Patchloom can be used as a Rust library (disable default cli feature for smaller dep). See patchloom::api (search_directory with context/globs/max_results, replace_text, read, doc_get / doc_keys / doc_len, etc) and execute_plan for tx. Full details and examples in crate docs and README "Embedding as a library". Recent expansions (#779 etc) and hygiene (#784) improved coverage.

  • Related: search, doc get, doc keys, doc len

status

  • What it does: Shows which files have uncommitted changes compared to git HEAD. This command is git-backed, so it must run inside a git repository.
  • Internal paths: Entries under .patchloom/ (backup sessions from --apply) are omitted so status reflects user project files, not Patchloom's undo store.
  • Use when: An agent needs a quick summary of the working tree before committing, staging, or choosing which files to process. For AI agents, native git status or terminal commands are typically equivalent.
  • Failure behavior: Outside a git repository (or if git status fails) exits 1 with error_kind: "invalid_input" under --json/--jsonl, and prints a git init hint in text mode.
  • Prefer instead: Use git status directly when you need full git porcelain output or staging details (including untracked .patchloom/ if you care about it).
  • Related: search, read, undo

undo

  • What it does: Previews or restores files from a backup created by a previous write --apply. Before any --apply write, patchloom saves originals under .patchloom/backups/<timestamp>/. Default is dry-run (same singularity as other write commands): bare patchloom undo prints what would be restored, exits 2 (CHANGES_DETECTED), and does not change files. Pass --apply to restore (exit 0). There is no --latest; without --session, the most recent backup is used. Dry-run JSON includes status: "changes_detected" and a hint field reminding agents to pass --apply. On case-insensitive filesystems (Windows NTFS, default macOS APFS), undo after a case-only rename (Hello.txt to hello.txt) restores the original directory-entry casing (#2345).
  • Use when: An --apply operation produced an undesirable result and you want to revert. Especially useful when the working tree was not committed before applying changes.
  • Notable flags:
    • --list shows available backup sessions. --json emits { "items": [...], "warnings": [...] } (each item still has timestamp, project_root, file_count, entries). --jsonl emits one session object per line, then a type: warnings trailer when listing warnings exist (no stderr).
    • --session <timestamp> targets a specific session (defaults to most recent).
    • --apply actually restores files (required for a real restore; omitted = preview only).
  • Failure behavior: No backup sessions (--list empty, or restore with no sessions) exits 3 (NO_MATCHES) with error_kind: "no_matches". If session directories exist but none have a readable manifest.json, --list exits 1 with error_kind: "invalid_input" (not no_matches) and the warning text (including the path) in the error message.
  • Agent trap: Do not treat exit 2 from bare undo as a completed restore. Re-run with --apply.
  • Prefer instead: Use git checkout or git stash when working in a committed git repo.
  • Related: tx, replace, tidy

explain

  • What it does: Parses a tx plan (JSON, YAML, or TOML) and prints a numbered, human-readable summary of each operation. Supports --json and --jsonl for structured output, plus --stdin for piped input. If both a path and --stdin are provided, stdin takes precedence and the path is ignored.
  • Use when: A user or agent wants to review what a tx plan will do before running tx --apply. Converts machine-readable plan format into plain English descriptions.
  • Prefer instead: Use tx directly (without --apply) to see the actual diff preview. Use explain when you want a quick overview without touching any files.
  • Related: tx, batch

schema

  • What it does: Exports the complete registry of patchloom operations with JSON Schemas, tier-filtered subsets, and LLM-ready system prompt fragments. Each operation is annotated with a minimum capability tier (weak, medium, strong).
  • Use when: You are building an AI agent that uses patchloom programmatically and need machine-readable operation schemas, or you want to generate a system prompt tailored to a specific model tier.
  • Notable flags:
    • --format json|prompt (default: json): json outputs operation schemas as JSON, prompt outputs markdown suitable for LLM system prompts.
    • --tier weak|medium|strong: Filter operations by minimum capability tier. Help lists these as the only allowed values (clap enum; no small/large aliases).
    • --examples: Include usage examples in JSON output (omitted by default).
  • Prefer instead: Nothing; this is the only programmatic way to discover available operations and their schemas.
  • Related: agent-rules, mcp-server

agent-rules

  • What it does: Prints an end-user AGENTS.md that teaches AI agents how to use patchloom. Includes command reference, exit codes, write modes, transaction plan format, and usage examples.
  • Use when: You are setting up a project where agents should use patchloom for file operations and need an AGENTS.md or SKILL.md that describes patchloom's interface.
  • Notable flags:
    • --mode cli|mcp|all (default: all): cli omits MCP section, mcp omits CLI shell examples, all includes everything.
    • --platform linux|windows|all (default: all): linux uses heredocs and single-quote syntax, windows uses file arguments and double-quote escaping, all shows both.
  • Prefer instead: Nothing; this is the only way to generate the end-user agent documentation.
  • Related: completions, mcp-server

init

  • What it does: Sets up patchloom in the current project: creates AGENTS.md if needed, otherwise appends the rules to an existing agent instructions file, prints shell completion instructions, detects MCP configuration opportunities, and ensures .gitignore ignores .patchloom/ (undo backups). When .vscode/ or .cursor/ already exists, it prints ready-to-copy .vscode/mcp.json or .cursor/mcp.json snippets.
  • Use when: You just installed patchloom and want a single command to configure a project instead of running agent-rules, completions, and MCP setup separately.
  • Notable flags:
    • -y, --yes: Skip confirmation prompts and auto-accept all actions (agent rules + shell completions install).
    • With global --json / --jsonl: agent-rules create/append is auto-accepted without -y so agent bootstrap does not report ok: true with agent_rules: skipped (#1833). Shell completion install still requires -y / interactive confirm.
    • Without -y and without --json/--jsonl, a non-interactive decline does not write AGENTS.md and reports agent_rules: skipped_use_yes (stderr names --yes / --json).
  • Prefer instead: agent-rules if you only need the rules text, or completions if you only need shell completions.
  • Related: agent-rules, completions, mcp-server, status, undo

mcp-server

  • What it does: Starts an MCP (Model Context Protocol) server, exposing patchloom operations as structured tool calls. Supports stdio (default) and Streamable HTTP transport (with --http). Included by default in all builds.
  • Use when: An MCP-capable AI agent can call patchloom tools directly via structured tool calls instead of constructing shell commands. Use --http for remote agents.
  • Notable flags:
    • --log <path>: Log tool calls to a JSONL file (also settable via PATCHLOOM_MCP_LOG env var).
    • --http: Use Streamable HTTP transport instead of stdio.
    • --host <addr> (default: 127.0.0.1): Bind address (requires --http). Non-loopback binds require --allow-unauthenticated.
    • --port <port> (default: 8080): Bind port (requires --http).
    • --allow-unauthenticated: Permit HTTP on a non-loopback bind. Streamable HTTP has no token.
    • --tls-cert <path> / --tls-key <path>: TLS certificate and key PEM files for HTTPS (requires --http; both must be provided together).
  • Environment: PATCHLOOM_MCP_SURFACE=core|full (env only; no CLI flag). Unset or full keeps the full inventory (product default). core registers the 11-tool pack for small agents. See README.md (MCP / coding agents).
  • Failure behavior: Invalid bind address, TLS config, or unauthenticated non-loopback --host without --allow-unauthenticated fails startup with error_kind: "invalid_input" when surfaced through typed error paths.
  • Prefer instead: Use the CLI directly when the agent does not support MCP, or when patchloom is invoked from scripts and CI.
  • Related: batch, tx

completions

  • What it does: Generates shell completion scripts for bash, zsh, fish, or elvish.
  • Use when: You are installing Patchloom into an interactive shell and want faster command discovery.
  • Prefer instead: Nothing, if Patchloom is only used from scripts or ephemeral CI runners.
  • Related: installation guide

ast

  • What it does: AST-aware operations on source code (20 languages). Subcommands: list (extract symbol definitions), read (read a symbol by name), rename (rename identifiers, skipping strings/comments), validate (syntax validation), search (structural queries), refs (find references), deps (extract imports), map (ranked repo map via PageRank), diff (structural diff vs git refs), impact (transitive impact analysis), replace (scoped text replacement within a symbol). list, read, replace, and validate use the same line numbering as search (LF, CRLF, and a lone CR).
  • Use when: You need to list, read, rename, validate, search, or analyze symbols with structural awareness (skip strings, comments, and documentation). Especially useful for rename operations where the old name appears inside strings that should not be changed, and for impact analysis before refactoring.
  • Prefer instead: Use replace --word-boundary for quick identifier renames when AST precision is not required. Use a language server (LSP) when cross-file type-aware rename is needed.
  • Related: replace, search

Command modes

These are meaningful command-specific modes that change how a top-level command behaves, even though they are not separate subcommands.

search --files-with-matches

  • What it does: Emits only file paths that contain at least one match.
  • Use when: You need a path list to feed into another tool or command instead of the matching lines themselves.
  • Prefer instead: Use search --count when per-file match totals matter, or plain search when the matching lines matter.

search --files-without-match

  • What it does: Emits only file paths that contain no matches (-L, same as grep -L). Combining with --files-with-matches (-l) or --count (-c) is invalid_input. When every scanned file contains the pattern, CLI --json and MCP search_files with files_without_match return error_kind: no_matches (CLI exit 3) and an empty files list. The error text is no files without matches for 'PATTERN' in SCOPE (not a content-miss "no matches" line, and not a -i tip). That is not a tool crash.
  • Use when: You need the complement of --files-with-matches: files in the scan that do not contain the pattern.
  • Prefer instead: Use --files-with-matches when you want files that hit, or --invert-match when you want non-matching lines rather than files with zero hits.

search --count

  • What it does: Emits match counts per file instead of full matching lines.
  • Use when: You are auditing prevalence, comparing files, or gating on how many matches remain.
  • Prefer instead: Use plain search when you need the matching text, or search --files-with-matches when only file membership matters.

search --invert-match

  • What it does: Shows lines that do not match the pattern.
  • Use when: You are looking for non-conforming lines or excluding content that matches a known pattern.
  • Prefer instead: Use plain search when you want the matching lines themselves.

search --multiline

  • What it does: Lets regex matches span multiple lines by making . match newlines.
  • Use when: The pattern you care about is inherently block-shaped, such as a function body or multi-line stanza.
  • Prefer instead: Use plain search for line-oriented patterns because it is simpler and easier to reason about.

search --before-context

  • What it does: Shows N lines before each match but none after (unless combined with -A).
  • Use when: You need to see what precedes a match (function signature before a body, imports before usage) without cluttering output with lines after.
  • Prefer instead: Use --context (-C) when symmetric context is fine, or combine -B and -A for independent before/after counts.

search --after-context

  • What it does: Shows N lines after each match but none before (unless combined with -B).
  • Use when: You need to see what follows a match (function body after signature, error handling after a call) without lines before.
  • Prefer instead: Use --context (-C) when symmetric context is fine, or combine -B and -A for independent before/after counts.

search --case-insensitive

  • What it does: Matches regardless of case.
  • Use when: The target text may appear in inconsistent capitalization across files.
  • Prefer instead: Use case-sensitive search when exact spelling matters and false positives would be noisy.

search --assert-count

  • What it does: Succeeds (exit 0) only if the total match count equals the given number. Exits 2 otherwise. Under --json/--jsonl, mismatch sets ok: false, status: "changes_detected", and error_kind: "changes_detected" (same kind as plan/tx search assert_count and MCP search_files).
  • Use when: An agent or CI pipeline needs to verify an invariant (e.g. "exactly 18 markers exist") in one call instead of searching and then comparing the count manually.
  • Prefer instead: Use plain search --count when you want to see counts without a pass/fail assertion.

search --max-results

  • What it does: Caps the detailed matches array under --json (and line-oriented output) while match_count stays the full total. Also caps the files list in --count, --files-with-matches, and --files-without-match modes (file_count stays full). When a list is capped, JSON sets truncated: true (omitted when complete). Tx plan search results use the same truncated field for content matches.
  • Use when: Agents need a bounded sample of hits or file paths without discarding the true total for budgeting or pagination (#1798).
  • Prefer instead: Omit --max-results when you need every match line or the full file inventory.

replace --regex

  • What it does: Treats the pattern as a regex instead of a literal string.
  • Use when: The change is pattern-based, or capture groups should shape the replacement.
  • Prefer instead: Use literal replace for fixed text because it is simpler and less error-prone.

replace --if-exists

  • What it does: Returns success when no content matches are found, and soft-skips missing explicit paths (CLI skipped[]; plan/batch path ops succeed with zero matches instead of hard not_found).
  • Use when: The replacement is intentionally idempotent and should not fail if the file or pattern is already gone or never present (for example optional config files in a batch).
  • Prefer instead: Use default replace behavior when a missing match or missing path should be treated as drift or an error.

replace --nth

  • What it does: Replaces only the Nth occurrence of the target.
  • Use when: Replacing every occurrence would be too broad and the exact positional match matters.
  • Prefer instead: Use plain replace when every occurrence should change, or regex when the target can be narrowed semantically.

replace --insert-before

  • What it does: Inserts text before each match instead of replacing it. The matched text is preserved. A payload that already ends in a newline is still one sibling line (no extra blank line). Same wrap as apply-fragment --before.
  • Use when: You need to add a line or annotation above an existing anchor without repeating the anchor in the replacement text.
  • Example: patchloom replace 'fn main() {' --insert-before '// entry' src/main.rs --apply
  • Prefer instead: Use --new when the matched text should actually change, not just receive a prefix.

replace --insert-after

  • What it does: Inserts text after each match instead of replacing it. The matched text is preserved. A payload that already ends in a newline is still one sibling line (no extra blank line). Same wrap as apply-fragment --after.
  • Use when: You need to append content after an existing anchor, such as adding a comment or tag after a specific line.
  • Example: patchloom replace 'use std::io;' --insert-after 'use std::fs;' src/main.rs --apply (anchor is positional OLD; insert text is on the flag; path last; never a second positional NEW).
  • Prefer instead: Use --new when the matched text should actually change, not just receive a suffix.

replace --multiline

  • What it does: Lets regex replacement span multiple lines by making . match newlines.
  • Use when: The target pattern is a multi-line block rather than a single line.
  • Prefer instead: Use line-oriented replace when the match should stay local and easy to inspect.

replace --case-insensitive

  • What it does: Matches regardless of case during replacement.
  • Use when: The target text appears with inconsistent capitalization and should still be updated uniformly.
  • Prefer instead: Use case-sensitive replace when exact spelling is part of the safety boundary.

replace --word-boundary

  • What it does: Wraps the search pattern with \b (word boundary) anchors so it only matches as a standalone word. Prevents SetupFile from matching inside BenchSetupFile. The pattern is auto-escaped for regex metacharacters before anchoring.
  • Use when: Renaming identifiers where the old name is a substring of other identifiers (e.g. SetupFile vs BenchSetupFile, Task vs TaskResult).
  • Prefer instead: Use AST-aware rename (#647) when you need to skip matches inside strings and comments. Word boundary only prevents partial-word matches, not string/comment matches.

replace --whole-line

  • What it does: Replaces (or deletes) entire lines that contain a match, instead of replacing only the matched span. When combined with --new '', removes matching lines entirely.
  • Use when: You need to delete lines matching a pattern (dead code, lint suppressions, debug statements) or replace full lines based on a partial match.
  • Prefer instead: Use regular replace when only the matched text should change while the rest of the line stays intact.

replace --range

  • What it does: Restricts --whole-line matching to a line range (e.g. --range 10:50). Lines outside the range are not considered for matching. Requires --whole-line.
  • Use when: The pattern matches lines you want to keep in other parts of the file (e.g. removing dead code from implementation but not from tests).
  • Prefer instead: Omit when the pattern is specific enough to avoid false positives.

replace --unique

  • What it does: Fails with exit code 5 (AMBIGUOUS) if the pattern matches more than once in any single file. Enforces unambiguous, single-target edits. The check is per-file: matching once in file A and once in file B is allowed.
  • Use when: You need a guardrail that the replacement targets exactly one location per file (CI scripts, automated pipelines, agent-driven edits where accidental bulk replacement is dangerous).
  • Prefer instead: Use --nth to target a specific occurrence when you know which one you want, or omit when replacing all occurrences is the intent.

replace --before-context

  • What it does: Provides context line(s) that must appear before the target for anchor-based disambiguation. When the pattern matches multiple times, the match nearest to this context is selected. Routes through the tx engine fallback chain, which supports fuzzy anchor matching when the exact text is not found.
  • Use when: The pattern matches multiple times in a file and you need to target one specific occurrence by its surrounding code. Requires explicit file paths (not directory scan).
  • Prefer instead: Use --nth when you know the ordinal position. Use --unique when you want to enforce single-match without specifying context.

replace --after-context

  • What it does: Provides context line(s) that must appear after the target for anchor-based disambiguation. Same semantics as --before-context but anchors on what follows the match instead of what precedes it. Both can be combined for even more precise targeting.
  • Use when: The pattern matches multiple times and the distinguishing context comes after the match, not before.
  • Prefer instead: Use --before-context when the preceding lines are more distinctive.

replace --require-change / library ReplaceOptions.require_change

  • What it does: Zero matches become an error (CLI exit 3 / structured EditErrorKind::NoMatch) instead of soft success. Softened by --if-exists / if_exists.
  • Use when: Agent hosts that treat a missed target as a tool error (fail closed). CLI already fails on no-match by default; the flag is explicit for plan/MCP/library parity.
  • Identity: When the pattern matches but new equals old, the match counts and require_change is satisfied (CLI exit 0 with an "identical (no file changes)" note). That is not a zero-match failure. With --json, the response includes "identity": true.
  • JSON error_kind: Soft no-match and --unique multi-match failures include error_kind: "no_matches" (exit 3) or error_kind: "ambiguous" (exit 5), matching tx plan JSON. Success responses omit the field.
  • Prefer instead: Leave the default when soft no-match is intentional. When both require_change and if_exists are set, if_exists wins.

replace --command-position / library ReplaceOptions.command_position

  • What it does: Replaces only tokens in shell command position (start of line, after && | ; / newlines, after wrappers like sudo, timeout 30, nice -n 10, setsid, unshare/nsenter/taskset/prlimit/numactl/chrt/setpriv, runuser -u USER, busybox, chpst -u app, softlimit -m N, flock /lock, chroot /jail, envdir /env, setlock /lock, xargs, eval, source, env KEY=val). Does not rewrite arguments (uv pip) or longer words (pipenv). Literal only. Also available on plan/MCP replace and batch_replace.
  • Use when: Migrating install tooling in shell scripts or agent-generated commands without breaking package names that embed the same substring.
  • Prefer instead: Ordinary replace or word_boundary for identifiers. Cannot combine with regex, case_insensitive, word_boundary, fuzzy, nth, multiline/whole-line, context anchors, or insert-before/after.

replace --fuzzy / library ReplaceOptions.fuzzy / plan fuzzy

  • What it does: When the exact pattern has zero matches, try similarity/anchor fallback (same chain as before/after context). Plan ops and MCP replace_text accept fuzzy: true. Pure fuzzy (no context) works on disk library, single-path tx, glob plan ops, and CLI (including directory roots expanded like ordinary replace).
  • Use when: Agent edits may have whitespace or small typos but should still land with honest match_mode / match_score / matched_text in library results and CLI/MCP JSON (#1669, #1736). Multi-file CLI replace, plan/tx, and content_edits all roll up worst-case confidence (fuzzy > anchored > exact) so mixed batches never under-report fuzzy. Aggregate match_score is the minimum fuzzy score across paths/ops (lowest confidence), not the first fuzzy hit. Aggregate matched_text is the widest span by Unicode char count (not first-non-null; #2007). Per-path spans stay on changes[].
  • Default safety (#1758): When exact old is absent, Similarity/fuzzy refuses to write by default (even above min_fuzzy_score) and reports the best candidate. Set --allow-absent-old / allow_absent_old only for deliberate approximate recovery. Anchored matches (explicit context) still apply.
  • Over-wide fuzzy refuse (#1981 / #2005 / #2008 / #2064): ReplaceOptions::for_agent() sets refuse_suspicious_fuzzy=true so replace_in_content auto-refuses over-wide fuzzy as EditErrorKind::FuzzySpanSuspicious (is_fuzzy_span_suspicious). Custom options: call api::fuzzy_span_suspicious(old, matched_text, match_score) (or FuzzySpanPolicy) before trust. Default policy: refuse when matched is wider than max(4 * old_chars, old_chars + 40), or score is in [0.90, 0.95) and ratio > 2. Buffer multi-op after apply_content_edits: refuse_batch_if_suspicious_fuzzy(&batch, &FuzzySpanPolicy::default()) (#2064). File multi-op with a final gate: apply_content_edits_to_file_with_span_policy(..., Some(&FuzzySpanPolicy::default())) refuses before write/backup (#2008).
  • Prefer instead: Exact replace when the target string is known; ast rename for code identifiers.

replace --min-fuzzy-score / library ReplaceOptions.min_fuzzy_score / plan min_fuzzy_score

  • What it does: When a fuzzy match is found, reject it if its similarity score is below this floor (0.0..=1.0). Exact and anchored matches are unaffected. Available on CLI (--min-fuzzy-score), plan/MCP (min_fuzzy_score), and ReplaceOptions (#1687).
  • Use when: Refuse weak similarity hits. Agent hosts using [ReplaceOptions::for_agent] get floor 0.90 (AGENT_MIN_FUZZY_SCORE). CLI examples may still use 0.80 as a looser floor.
  • Does not mean: score >= min_fuzzy_score alone authorizes a write when exact old is absent; that still requires allow_absent_old (#1758).
  • Prefer instead: Exact replace when the target string is known.

replace --allow-absent-old / library ReplaceOptions.allow_absent_old / plan allow_absent_old

  • What it does: Opt in to historical fuzzy behavior: when exact old is not in the file, apply the best Similarity candidate above min_fuzzy_score (if any). Default is false (fail closed; no write).
  • Use when: You intentionally want approximate recovery and will verify matched_text.
  • Prefer instead: Leave unset for agent hosts; use exact strings or AST renames.

library ReplaceOptions::for_agent / AGENT_MIN_FUZZY_SCORE

  • What it does: Shared constructor for coding-agent hosts so primary and fallback replace paths use one policy (#1965): unique=true, require_change=true, fuzzy=true, min_fuzzy_score=Some(0.90) (AGENT_MIN_FUZZY_SCORE), allow_absent_old=false.
  • Use when: Embedding patchloom in an agent runtime with more than one replace call site. Prefer ReplaceOptions::for_agent() over hand-copied ReplaceOptions { ... } blocks that can drift.
  • Over-wide fuzzy: for_agent includes refuse_suspicious_fuzzy=true (auto-refuse). Multi-op: use ContentEditsResult.op_honesty for per-replace old + span (#2006), or refuse_batch_if_suspicious_fuzzy for the full batch gate (#2064). File Apply with pre-write gate: apply_content_edits_to_file_with_span_policy (#2008). Custom options still call api::fuzzy_span_suspicious before trust (#1981).
  • Overrides: Struct update: replace-all → unique: false; deliberate approximate recovery → allow_absent_old: true; word-boundary rename → fuzzy: false, min_fuzzy_score: None, word_boundary: true.
  • Not: A host-specific recovery preset with allow_absent_old: true. Fail-closed missing-old remains the library default; recovery stays an explicit override (#1980 closed not planned).
  • Prefer instead: ReplaceOptions::default() only for non-agent / soft no-match library callers.

create --stdin

  • What it does: Reads the new file content from stdin instead of --content.
  • Use when: Another tool is generating the content, or shell composition is cleaner than embedding the full text in one argument.
  • Prefer instead: Use create --content for short inline content that should stay visible in the command itself.

create --force

  • What it does: Overwrites an existing file instead of failing.
  • Use when: File recreation is intentional and should replace previous contents deterministically.
  • Prefer instead: Use default create behavior when accidental overwrite would be dangerous.

patch FILE

  • What it does: Reads the unified diff from a file path (positional argument).
  • Use when: The patch already exists as a saved artifact that should be reviewed, reused, or passed around directly.
  • Prefer instead: Use patch --stdin when another tool is piping the patch text dynamically.

patch --stdin

  • What it does: Reads the unified diff from stdin instead of a file argument.
  • Use when: Another tool is generating or piping the patch text directly.
  • Prefer instead: Use patch FILE when the diff should be stored as a tangible artifact.

patch apply --replace-all

  • What it does: For SEARCH/REPLACE / DiffFenced documents only, updates every exact match instead of requiring a unique match.
  • Use when: The SEARCH block is intentionally repeated and every occurrence should change.
  • Prefer instead: Default unique apply when a multi-match would be accidental.
  • Failure: Unified diff or Begin Patch with --replace-all / replace_all is invalid_input (no write).

doc --predicate

  • What it does: Supplies the key-value predicate used by doc delete-where. Object arrays use a field key (e.g. name=react). Scalar arrays match the element with .=a, _=a, or value=a (value is accepted as an agent-friendly alias for element match).
  • Use when: Array cleanup should target matching objects or scalar values instead of deleting by fixed index or selector path alone.
  • Prefer instead: Use doc delete when one direct selector path can remove the target without predicate filtering.

doc --stdin

  • What it does: Reads merge payload content from stdin for doc merge.
  • Use when: The object being merged is generated by another tool or is awkward to express inline.
  • Prefer instead: Use doc merge --value for short, self-contained object literals.

md --stdin

  • What it does: Reads replacement or inserted markdown content from stdin for the section-editing commands.
  • Use when: The markdown payload is generated, large, or easier to stream than to quote inline.
  • Prefer instead: Use --content when the inserted text is small and should stay visible in the command.

tx -

  • What it does: Reads the transaction plan from stdin instead of a plan file. Defaults to JSON; use --plan-format for YAML or TOML.
  • Use when: The plan is generated on the fly or piped from another tool.
  • Prefer instead: Use tx FILE when the plan should be stored, reviewed, or reused.

tx --plan-format yaml

  • What it does: Tells tx to parse the plan as YAML (or TOML) instead of JSON. Auto-detected from file extension for plan files; required when piping YAML from stdin.
  • Use when: The plan is easier to write or generate in YAML/TOML, or when JSON verbosity is friction for inline agent-generated plans.
  • Prefer instead: Use JSON plans when interoperability or strict schema validation matters more than writability.

doc actions

Use these when the top level doc command is right, but you need a specific structured operation.

Comment and structure preservation: All doc write operations preserve inline comments, section comments, and formatting in YAML and TOML files. The parser edits the concrete syntax tree (CST) directly, so only the changed values are rewritten while surrounding comments and whitespace stay intact. This includes operations that change array length (append, prepend, delete-where), which use text-level splicing to preserve comments on the affected file. For YAML, anchors (&name), aliases (*name), and merge keys (<<: *name) are kept when the edit does not require expanding them (for example, changing a sibling field that is not part of the shared defaults). Emptying one or more sequence items (including a document-root list, or nested arrays to []) keeps those anchors and comments instead of dumping the document. Local overrides of merge-inherited keys add an explicit key beside << rather than exploding the merged map. An interior edit of a pure mapping alias (service_a: *shared or a list item - *shared) becomes a merge (<<: *shared plus the local key) so sibling aliases stay aliases. Deleting an inherited key, or replacing the alias with a map that does not keep every inherited key, still writes a concrete map for that site; unrelated structure is left alone.

Multi-document YAML: Streams with more than one --- document are modeled as a JSON array (one element per document). Use a document index in the selector (0.metadata.name, [1].spec.ports[0].port). A bare top-level key on a multi-doc file fails with an actionable type error that points at the index form. doc merge of any overlay (object or array) into the multi-doc root is also type_error (would replace the whole stream). Successful writes re-serialize with --- separators (not as a single YAML sequence), so kubectl apply -f style multi-doc files stay valid.

Selector predicates: Filter array items (or values of an object map) inside a selector path.

  • = equality. Unchanged from historical key=value. The value may contain = or > (items[url=a=b], items[url=a>b]).
  • != not-equal (items[status!=disabled]). A missing field does not match.
  • >, >=, <, <= numeric compare on JSON numbers or numeric strings (servers[port>8000]). The operand after the operator must parse as a number ([port>abc] is invalid_input). A present non-numeric field vs one of these operators is invalid_input, not a lexicographic compare. A missing field does not match.
  • [!key] matches when key is absent, JSON false, or null (flags[!deprecated]).
  • Regex predicates are not supported.

Predicates can be chained: data[type=server][port>8000].

JSON write summary: Every doc write success payload under --json / --jsonl includes changed (bool). doc delete and doc delete-where also include removed (usize). Agents should not treat exit 0 alone as "something was deleted"; check removed / changed for idempotent no-ops. The same fields appear on MCP doc_delete / doc_delete_where and on execute_plan / tx reports (plus a mutations array with per-op path, op, changed, removed for multi-step plans).

doc get

  • What it does: Reads the value at a selector path from a JSON, YAML, or TOML file.
  • Use when: You need one precise value without mutating the document.
  • JSON success (#1838): Under --json/--jsonl, success is {"ok":true,"value":...,"path":...,"selector":...} (not a bare JSON value). Text mode stays bare. Missing selector is error_kind: no_matches (exit 3). A bare top-level key on multi-document YAML (array root) is error_kind: type_error (exit 1) with an index-form hint (0.key / [0].key), same as doc set on that shape.
  • Prefer instead: Use doc flatten when you are exploring an unfamiliar file and need a broader map of its contents.

doc has

  • What it does: Checks whether a selector path exists.
  • Use when: A script or workflow needs a presence check before choosing a later action.
  • Exit codes (#1843): Always exits 0 for a valid document when the answer is true or false. Missing key is not no_matches. Real failures (missing file, parse error) still fail non-zero.
  • JSON success (#1838): {"ok":true,"value":true|false,"path":...,"selector":...} under --json/--jsonl.
  • Prefer instead: Use doc ensure when the real goal is to create the value if it is missing.

doc keys

  • What it does: Lists the keys of an object at a selector path.
  • Selector: Optional; defaults to . (document root).
  • Use when: You want to inspect the shape of a structured object before choosing an edit.
  • Prefer instead: Use doc get when you already know the exact selector path you want.

doc len

  • What it does: Counts items in an array or object.
  • Selector: Optional; defaults to . (document root).
  • Use when: You need a quick cardinality check in scripts, CI, or exploratory work.
  • Prefer instead: Use doc select or doc get when the actual values matter more than the count.

doc set

  • What it does: Sets or creates a value at a selector path.
  • Use when: One exact selector path should be updated deterministically.
  • Prefer instead: Use doc merge for multi field updates, or doc ensure when existing values should be preserved. Use doc update for predicate or wildcard multi-match (items[name=a].v, items[*].enabled).
  • Failure behavior: Predicate or wildcard selectors fail closed (exit 1) with error_kind: "invalid_input" and machine-stable suggested_op: "doc.update" under --json / plan / MCP (#2133). The same mapping applies when the predicate is an intermediate parent segment (e.g. items[id=a].val), not only a leaf (#2138).
  • Leading slash: A single leading / is stripped (JSON Pointer habit). /feature_flag sets key feature_flag, not a key named /feature_flag (#1794). Prefer bare keys in agent prompts.
  • Root selector: . (also empty or /) is the document root. doc keys FILE . lists top-level keys. doc set FILE . VALUE replaces the whole document.

doc delete

  • What it does: Removes the value at a selector path.
  • Use when: A selector path or node is obsolete and should disappear cleanly.
  • Idempotency: When the selector matches nothing, the command exits 0 and does not rewrite the file.
  • JSON summary: With --json / --jsonl, success payloads include changed (bool) and removed (1 when a value was deleted, 0 on no-match). Exit 0 with "removed": 0 is expected for idempotent cleanup of a missing key.
  • Failure behavior: Predicate or wildcard on a single-path delete (leaf or intermediate parent, e.g. items[id=a].val) fails closed with error_kind: "invalid_input" and suggested_op: "doc.delete_where" under --json / plan / MCP (#2133 / #2138). Prefer doc delete-where or a concrete index path.
  • Prefer instead: Use doc delete-where when the target is a subset of array items instead of one direct selector path.

doc delete-where

  • What it does: Deletes array items that match a predicate (--predicate key=value). For scalar arrays, use .=x, _=x, or value=x. This is a separate filter from selector predicates used by doc update.
  • Use when: You need to remove selected objects or scalar values from a list without rebuilding the whole array by hand.
  • Idempotency: When no elements match the predicate, the command exits 0 and does not rewrite the file (same as doc delete on a missing key). Use doc update when a missing match should be an error.
  • JSON summary: With --json / --jsonl, success payloads include changed (bool) and removed (usize). Exit 0 with "removed": 0 and "changed": false means the predicate matched nothing (idempotent cleanup). A non-zero removed means that many array items were deleted.
  • Prefer instead: Use doc delete when one direct selector path can remove the target.

doc merge

  • What it does: Deep merges an object payload into an existing document.
  • Use when: Several related fields should be added or updated together.
  • Prefer instead: Use doc set when one exact path should change and merge semantics are unnecessary.

doc append

  • What it does: Appends a value to an array.
  • Use when: New items should appear at the end of the list.
  • Prefer instead: Use doc prepend when order or precedence means the new item should come first.

doc prepend

  • What it does: Inserts a value at the front of an array.
  • Use when: The new item should win by order, or defaults should be introduced at the front of the list.
  • Prefer instead: Use doc append when simple chronological growth is enough.

doc select

  • What it does: Reads only the values that match a selector path or predicate.
  • Use when: You need a filtered read view of a larger structure.
  • Prefer instead: Use doc update or doc delete-where when the end goal is mutation rather than inspection.

doc update

  • What it does: Sets the same new value at every location matching a selector. Wildcards (items[*].enabled) and selector predicates (items[name=foo].v) filter inside the selector string. There is no separate --where or --predicate flag (unlike doc delete-where).
  • Use when: A broad but uniform change should apply across many selected elements.
  • Prefer instead: Use doc set when the change only targets one path.

doc move

  • What it does: Moves or renames a selector path.
  • Use when: Schema cleanup or path migration should preserve the value while changing the selector path.
  • Prefer instead: Use doc set plus doc delete only when the move semantics are not a clean fit.

doc ensure

  • What it does: Creates a value only if it is currently missing.
  • Use when: You need idempotent config bootstrapping and must not overwrite existing values.
  • Prefer instead: Use doc set when the desired value should win even if the selector path already exists.

doc flatten

  • What it does: Lists leaf selector paths and their values.
  • Use when: You are discovering the shape of an unfamiliar structured file.
  • Prefer instead: Use doc get for one targeted read, or doc keys when only the object shape matters.

doc diff

  • What it does: Compares two structured files by their semantic content.
  • Use when: You care about structural value changes more than raw formatting differences.
  • Prefer instead: Use patch or ordinary diff tooling when the exact textual patch matters.

md actions

Use these when markdown structure matters more than raw text matching.

md replace-section

  • What it does: Replaces the body of a heading section. The section runs from after the matched heading until the next heading of the same or higher level (CommonMark hierarchy). Nested lower-level headings (for example ## API under # Intro) are part of that section and are replaced too.
  • Use when: A section should be treated as authoritative content that can be rewritten in one step. Prefer peer-level headings (all ##) when you must keep sibling sections.
  • Prefer instead: Use md insert-after-heading when existing section content should stay and you only need to add more text. Target a ## heading when you only want that subsection, not a parent # that owns nested ## children.

md insert-after-heading

  • What it does: Inserts content immediately after the heading line (before any existing body such as tables or paragraphs). It does not insert after the full section body.
  • Use when: You want to add a note, intro, or status line under a heading while keeping the rest of the section body after the insert (for example intro text before an existing table).
  • Prefer instead: Use md insert-after-section when adding a sibling ## section after this section's body. Use md replace-section when the whole section should be regenerated.

md insert-after-section

  • What it does: Inserts content after the full section body (after the last line of the section, before the next same-or-higher heading). Sibling placement for new sections.
  • Use when: You want to add a new ## FAQ (or similar) after ## Config including Config's existing body.
  • Prefer instead: Use md insert-after-heading only for content under the heading line, not for a new sibling section.

md insert-before-heading

  • What it does: Inserts content immediately before a heading line.
  • Use when: You want to add a preface or a new section boundary before an existing heading.
  • Prefer instead: Use md insert-after-section when the addition belongs after the previous section's body.

md upsert-bullet

  • What it does: Ensures a bullet exists under a heading, without duplicating it.
  • Use when: Rules, checklists, or recurring notes should be added idempotently.
  • Prefer instead: Use md replace-section when the entire list should be rewritten.

md dedupe-headings

  • What it does: Removes later whole sections whose heading text and level match an earlier heading (heading line plus body until the next same-or-higher heading). It does not only delete the duplicate heading line; unique content under the second heading is removed with it.
  • Use when: Generated markdown or hand edited docs have accumulated repeated sections that should collapse to one, and keeping only the first section body is correct.
  • Prefer instead: Use md lint-agents when the goal is diagnosis rather than mutation. Do not use dedupe if later duplicate-titled sections hold unique content you need to keep or merge.
  • JSON: Object with ok, path, removed (heading strings), applied (false for preview/check), and backup_session after a real apply. JSONL still emits one JSON string per removed heading.

md lint-agents

  • What it does: Checks AGENTS style markdown for common problems.
  • Use when: You want a CI style guard for agent instruction files before they drift into invalid or confusing structure.
  • Prefer instead: Use md dedupe-headings when you already know the file should be auto corrected.
  • JSON (#1854 / #1859): Object envelope {ok, path, issue_count, issues} (single-file diagnostic; MCP md_lint uses the same shape). tidy check --json is multi-file: {ok, issue_count, issues} with path on each issue (no top-level path). ok is true when there are no issues. CLI exit 2 when issue_count > 0. MCP keeps the tool call successful (isError=false) and agents branch on ok / issue_count. JSONL still emits one issue object per line (no envelope).

md table-append

  • What it does: Appends a row to the markdown table under a heading.
  • Use when: A docs table should grow without manually rebuilding its existing rows.
  • Prefer instead: Use md replace-section when the whole table should be regenerated from source data.

md move-section

  • What it does: Moves a heading section to a new position, either within the same file (reorder) or to a different file. The section (heading plus body) is extracted from the source and inserted at the target location. Both files are updated atomically.
  • Use when: Reorganizing documentation structure by moving sections between files or reordering sections within a file.
  • Prefer instead: Use md replace-section for rewriting content in place, or manual cut and paste when the move involves non-contiguous content.

patch actions

Use these when the change already exists as a unified diff.

patch check

  • What it does: Dry-run a unified diff without writing. Per-file status is would_change (exit 2) when the patch applies and content would change (including pure git renames, 100% copies, and empty creates where content is identical but the dest is new), unchanged when the result equals the current file, or fail-closed statuses for problems (missingnot_found, rename/copy/empty-create dest exists → already_exists, unsupported git-meta → invalid_input, staleambiguous / exit 5).
  • Use when: CI or agents need the same “would change” signal as patch apply preview before committing to --apply.
  • Prefer instead: Use patch apply when the patch should be written, or replace and doc when you do not actually need to carry a diff file.
  • JSON: Includes applied: false. Do not treat historical status clean as “nothing to do”; that name meant “applies without fuzz” and confused agents.

patch apply

  • What it does: Applies a unified diff, including git renames with hunks, pure renames (similarity index 100% with rename from / rename to), and 100% git copies (copy from / copy to, dest created, source kept). Also detects Codex Begin Patch and Aider SEARCH/REPLACE / DiffFenced (<<<<<<< SEARCH). SEARCH/REPLACE is unique unless --replace-all. Optional C-quoted paths with spaces and octal escapes. Git-meta binary / mode-only dests are listed for preflight then apply refuses. Use --on-stale merge to retry with three-way merge when context is stale. Empty-hunk +++ /dev/null (git deleted file mode, no hunks) unlinks. A hunked delete applies the minus lines first; leftover bytes rewrite the file (preview --diff to see them). Path-only unlink is file.delete.
  • Use when: The desired change is already available as patch text, a Begin Patch envelope, or a SEARCH/REPLACE document and should be replayed directly.
  • Failure behavior: Missing rename/source target → not_found. Git rename or copy destination already present → already_exists (same policy as rename without --force; remove the dest or use file.rename --force). Unsupported git-meta (binary payload, mode-only) → invalid_input (dest still appears in parse_unified_diff / patch_declared_paths). Stale minus lines on a hunked delete are ambiguous (exit 5) and the file is not removed; regenerate minus lines from the current file, or use file.delete for path-only unlink.
  • Prefer instead: Use replace, md, or doc when you would rather describe the desired mutation at a higher level.

patch merge

  • What it does: Three-way merges a unified diff. Conflicts emit <<<<<<< patchloom (ours) / ======= / >>>>>>> patch (theirs) markers.
  • Use when: Patch context is stale but you still want partial replay instead of regenerating the diff.
  • Flags: --check reports clean/merged/conflict per file. Conflicts block --apply unless --allow-conflicts. Exit 8 (CONFLICTS) when conflicts remain.

tidy actions

Use these when newline and whitespace correctness is the main concern.

tidy check

  • What it does: Reports missing final newlines, mixed line endings, and trailing whitespace in text files. Binary and invalid UTF-8 files are skipped.
  • Use when: You want a non mutating tidy audit for CI or local review.
  • Prefer instead: Use tidy fix when the goal is to normalize the files immediately.

tidy fix

  • What it does: Applies newline and whitespace normalization to text files. Binary and invalid UTF-8 files are skipped. With no write-policy flags (and without --respect-editorconfig), it enables final-newline and trailing-whitespace fixes so it matches the issues bare tidy check always reports. Pass explicit flags (or EditorConfig) to narrow the fix set.
  • Use when: Existing files already need cleanup and the cleanup itself is the task.
  • Prefer instead: Use write policy flags on another write command when normalization should only apply to files already being touched by that command.

tx reference

tx is the place where Patchloom's features compose. Use Core Concepts for the canonical explanation of rollback and exit codes, and examples for plan templates.

Plan fields

version

  • What it does: Declares the plan schema version. Patchloom rejects plans whose version does not match the version it supports.
  • Use when: Every plan must include this field. It ensures forward-compatibility safety so an old patchloom build does not silently misinterpret a plan written for a newer schema.
  • Required: Yes. Plans without a version field are rejected.

cwd

  • What it does: Sets the base directory used to resolve relative paths inside the plan.
  • Use when: You need plan operations and lifecycle steps to run from a specific subdirectory under the invocation root.
  • Important: Relative values resolve from the invocation working directory (--cwd or the process cwd), not from the plan file's directory. In MCP mode, plan.cwd must be a relative path under the server workspace root; it is honored for op path re-rooting. Absolute path strings, ../ escapes, and combining cwd with for_each are rejected as invalid params (not silently stripped). If the resolved path does not exist or is not a directory, the plan is rejected with PARSE_ERROR (exit 4).
  • Prefer instead: Use the CLI --cwd flag when the directory choice is a caller concern rather than part of the plan itself.

write_policy

  • What it does: Applies newline, EOL, and whitespace normalization across all pending writes in the plan.
  • Use when: Every write in the transaction should share the same normalization policy.
  • Fields: Supports ensure_final_newline (bool), normalize_eol (keep, lf, crlf, or cr), trim_trailing_whitespace (bool), and collapse_blanks (bool).
  • Failure behavior: Invalid normalize_eol values exit 1 with error_kind: "invalid_input" (not operation_failed / 9).
  • Precedence: Patchloom starts from the invocation's per-file write policy, including CLI flags and any --respect-editorconfig values, then overrides only the keys set here.
  • Prefer instead: Use CLI write flags when one invocation needs defaults, but the plan itself should stay generic.

strict

  • What it does: Rolls back file writes when a format or validation step fails. Defaults to true when omitted from the plan.
  • Use when: Partial writes are unacceptable and post-write failure should behave like a full transaction failure (the default for agent workflows).
  • Prefer instead: Set "strict": false in the plan, [tx] strict = false in .patchloom.toml, or patchloom tx plan.json --apply --no-strict when writes may stay on disk even if later validation reports a problem.

operations

  • What it does: Lists the ordered mutations that make up the transaction.
  • Alias: ops is accepted on deserialize (common agent shorthand). Serialized plans still emit operations.
  • Use when: One logical change spans several steps or several mutation types.
  • Prefer instead: Use a standalone command when one direct operation is enough.

format

  • What it does: Runs shell commands after writes are staged to disk but before validation.
  • Use when: Generated or edited files should be normalized by tools like cargo fmt, prettier, or black as part of the same workflow.
  • Step fields: Each entry accepts cmd (required shell command) and timeout (seconds, default 60).
  • Failure behavior: Any non-zero exit or timeout fails the transaction. Error output reports the failing step number, exit status, the lifecycle working directory (cwd), and a truncated snippet of the command's stderr when available. With strict: true, Patchloom rolls back the staged writes.
  • Prefer instead: Run formatting outside tx when it does not need to participate in the transaction's success criteria.

validate

  • What it does: Runs shell commands that decide whether the transaction should be reported as valid.
  • Use when: Build, test, or policy checks are part of the definition of success for the change.
  • Step fields: Each entry accepts cmd (required shell command), required (bool, default false), and timeout (seconds, default 60).
  • Failure behavior: required: true makes the step gate transaction success. required: false still reports the validation problem to stderr. Error output reports the failing step number, exit status, the lifecycle working directory (cwd), and a truncated snippet of the command's stderr when available.
  • Prefer instead: Use standalone verification outside tx when the mutation and the validation lifecycle should stay separate.

verify

  • What it does: Runs pre/post-operation symbol verification checks to ensure structural safety.
  • Use when: A refactoring plan must preserve the number of functions, test methods, or other AST symbols.
  • Field value: Array of check objects. Each is either {"kind": "function", "attr": "test"} (symbol count) or {"check": "unique_names"} (named check).
  • CLI equivalent: --verify="kind=function,attr=test" (repeatable).
  • Failure behavior: When a check fails, the transaction rolls back and exits with VALIDATION_FAILED (6).
  • Prefer instead: Omit when the plan only touches configuration files or non-code content.

for_each

  • What it does: Glob-driven batch expansion. When present, the plan's operations are treated as templates and expanded once per matching file. Template variables ({path}, {item} as an alias for {path}, {dir}, {stem}, {ext}, {name}) are substituted in all operation fields. A path field that is still a lone {placeholder} after expansion is rejected as invalid_input (not a later opaque not_found).
  • Escape mechanism: Double the braces to produce a literal brace in the output. {{path}} becomes {path} (not substituted), {{stem}} becomes {stem}, etc. Use this when operation values must contain literal brace-wrapped text that should not be treated as template variables.
  • Use when: The same structural transform (extract tests, add headers, reorder symbols) must be applied to many files matching a glob pattern.
  • Field value: Object with glob (required), exclude (optional array of glob patterns), and filter (optional, e.g. has_symbol(tests)).
  • MCP: Do not set for_each together with plan.cwd (rejected). Use workspace-relative {path} templates without cwd.
  • Failure behavior: A zero-match glob is no_matches (exit 3 / EditErrorKind::NoMatch), not a successful empty apply. Combining plan.cwd with for_each is invalid_input. A glob (or exclude glob) that globset cannot parse, and a filter other than has_symbol(NAME), are invalid_input (exit 1), not parse_error (exit 4). If any expanded operation fails, the entire batch rolls back atomically.

Transaction operations

The operations below are the building blocks inside operations.

replace

  • What it does: Runs text replacement inside a transaction.
  • Use when: A text rewrite needs to share atomic rollback, formatting, or validation with other operations.
  • Requires: Exactly one of to, insert_before, or insert_after, matching top level replace.
  • Regex insert semantics: In regex mode, insert_before and insert_after preserve the matched text, they do not insert the raw pattern string.
  • Optional fields: case_insensitive (bool, default false), multiline (bool, default false), and if_exists (bool, default false) match the top level replace --case-insensitive, --multiline, and --if-exists flags. Library-aligned plan fields: require_change (bool, default false; hard-fails the op on zero matches when if_exists is false) and command_position (bool, default false; shell invocable rewrite).
  • Related: top level replace

apply.fragment

  • What it does: Applies a freeform fragment with a required placement anchor inside a transaction (Morph-style // ... existing code ... lines stripped).
  • Use when: Batching Morph-class freeform snippets with known after/before/old anchors.
  • Requires: Exactly one of after, before, or old. Non-empty fragment after marker strip. unique defaults true.
  • Related: CLI apply-fragment, MCP apply_fragment, top level replace

doc.set

  • What it does: Runs a targeted structured set inside a transaction.
  • Use when: A precise config update must be bundled atomically with other repo changes.
  • Field naming: Use selector for the path expression in doc.set, doc.delete, doc.append, doc.prepend, doc.update, doc.ensure, and doc.delete_where.
  • Optional fields: if_exists (bool, default false). When true, a missing file or missing selector is a soft success (no write). The key is not created. Default remains fail-hard (not_found for a missing file; a missing selector creates the key).
  • Related: top level doc set

doc.delete

  • What it does: Removes a structured value inside a transaction.
  • Use when: Schema cleanup should happen as one step in a larger atomic change.
  • Related: top level doc delete

doc.merge

  • What it does: Deep merges structured content inside a transaction.
  • Use when: Several related structured fields should change together as part of one plan.
  • Related: top level doc merge

doc.append

  • What it does: Appends to an array inside a transaction.
  • Use when: List growth must stay atomic with other edits in the same plan.
  • Related: top level doc append

doc.prepend

  • What it does: Prepends to an array inside a transaction.
  • Use when: Ordered config precedence should change as part of a larger atomic mutation.
  • Related: top level doc prepend

doc.update

  • What it does: Updates all matching structured nodes inside a transaction. Matching is via the selector field (wildcards and selector predicates), not a separate predicate field.
  • Use when: A broad structured rewrite should be coupled to other edits and validations.
  • Failure behavior: When the selector matches nothing, the plan exits 3 (no_matches); unlike idempotent doc.delete, a miss is an error.
  • Related: top level doc update

doc.move

  • What it does: Moves or renames a structured selector path inside a transaction.
  • Use when: Schema migration must stay atomic with related code or docs edits.
  • Related: top level doc move

doc.ensure

  • What it does: Adds a structured value only if it is missing, inside a transaction.
  • Use when: Idempotent bootstrapping should happen together with other plan steps.
  • Related: top level doc ensure

doc.delete_where

  • What it does: Deletes array items matching a predicate inside a transaction.
  • Use when: Targeted list cleanup must be coordinated with other transactional edits.
  • Related: top level doc delete-where

md.replace_section

  • What it does: Replaces a markdown section body inside a transaction. The section ends at the next heading of the same or higher level; nested lower-level headings are included in the replaced range.
  • Use when: Docs regeneration should be part of a larger all or nothing repo change. Prefer peer-level headings when siblings must survive.
  • Failure behavior: Missing heading exits 3 (no_matches) with the heading name in the error.
  • Related: top level md replace-section

md.insert_after_heading

  • What it does: Inserts markdown content immediately after a heading line (before existing body) inside a transaction.
  • Use when: A release note or docs annotation under a heading must be added atomically with code or config changes.
  • Related: top level md insert-after-heading; sibling sections: md.insert_after_section

md.insert_after_section

  • What it does: Inserts markdown content after the full section body (sibling placement) inside a transaction.
  • Use when: Adding a new section after an existing section's content as part of a multi-op plan.
  • Related: top level md insert-after-section

md.insert_before_heading

  • What it does: Inserts markdown content before a heading line inside a transaction.
  • Use when: Docs structure must change as one step in a broader plan.
  • Related: top level md insert-before-heading

md.upsert_bullet

  • What it does: Ensures a markdown bullet exists inside a transaction.
  • Use when: Idempotent docs or checklist updates should stay coupled to other edits.
  • Related: top level md upsert-bullet

md.table_append

  • What it does: Appends a markdown table row inside a transaction.
  • Use when: Documentation tables should be updated together with the code or metadata they describe.
  • Related: top level md table-append

md.move_section

  • What it does: Moves a markdown section to a new position, optionally to a different file. The moved range ends at the next heading of the same or higher level; nested lower-level headings move with the parent.
  • Use when: Section reordering or cross-file moves should be atomic with the rest of the plan.
  • Related: top level md move-section

md.dedupe_headings

  • What it does: Removes later whole sections whose heading text+level already appeared (heading and body until the next same-or-higher heading). Unique content under the second heading is discarded, not merged.
  • Use when: Cleanup of generated docs should stay atomic with the rest of the plan. Do not use when duplicate headings intentionally hold different bodies that must both survive.
  • Related: top level md dedupe-headings

md.lint_agents

  • What it does: Lints an AGENTS.md file for common problems (duplicate headings, dangerous commands outside code fences, missing final newline) inside a transaction.
  • Use when: Agent rules validation should be part of a larger plan, e.g., lint before and after markdown edits to confirm no new issues.
  • Related: top level md lint-agents, MCP md_lint

tidy.fix

  • What it does: Applies tidy normalization inside a transaction.
  • Use when: Text cleanup should be part of the same atomic success criteria as other edits.
  • Defaults (#1840): When the op omits write-policy fields, matches bare CLI tidy fix: trim trailing whitespace and ensure final newline (normalize_eol stays keep unless set). Precedence: tidy defaults → plan write_policy → op fields. At commit, plan write_policy is not re-applied to paths last written by tidy.fix so op fields stick (#1847); CLI/EditorConfig policy still applies. A later non-tidy write clears that and restores full plan policy.
  • Related: top level tidy fix

file.append

  • What it does: Appends content to the end of an existing file inside a transaction. Inserts a newline separator if the file does not end with one.
  • Use when: Adding content to a file must be atomic with other operations in the same plan. Fails if the file does not exist.
  • Related: top level append

file.prepend

  • What it does: Prepends content to the beginning of an existing file inside a transaction.
  • Use when: Adding a header, license, or shebang line must be atomic with other operations in the same plan. Fails if the file does not exist.
  • Related: file.append, top level append

file.create

  • What it does: Creates a file inside a transaction.
  • Use when: New files must appear only if the full plan succeeds.
  • Related: top level create

file.delete

  • What it does: Deletes a file inside a transaction.
  • Use when: File removal should roll back if later format or validation steps fail.
  • Optional fields: if_exists (bool, default false). When true, a missing file is a soft success (no write). Default remains fail-hard (not_found).
  • Related: top level delete

file.rename

  • What it does: Renames (moves) a file, symlink, or special node inside a transaction. Path-only for non-regular files so symlink targets are never rewritten by write policy.
  • Use when: File renames should roll back if later format or validation steps fail. More efficient than read + file.create + file.delete as a single operation.
  • Related: top level rename

search

  • What it does: Searches a file for a pattern inside a transaction and includes match results in the JSON output without writing anything.
  • Use when: An agent needs to locate patterns before replacing them in the same plan, enabling locate-then-edit in a single call.
  • Optional fields: literal, regex, case_insensitive, multiline, invert_match, context/before_context/after_context, globs, exclude_patterns, custom_ignore_filenames (for agent/tool ignore layering), max_results, assert_count. Match and ignore options match the top-level search command. File-list modes (--files-with-matches / --files-without-match / --count) are CLI and MCP search_files only.
  • Related: top level search

read

  • What it does: Reads a file inside a transaction and includes its content in the JSON output without writing anything. The JSON read result carries the same line metadata as top level read (start_line, end_line, total_lines), and when no line range is requested it preserves the raw file content exactly.
  • Use when: An agent needs to inspect file content before or after other operations in the same plan, enabling "understand then edit" in a single call.
  • Related: top level read

patch.apply

  • What it does: Applies a unified diff inside a transaction. Supports on_stale: "merge" for three-way merge when the on-disk file diverged from the patch base, and allow_conflicts: true to write conflict markers instead of failing during staging. Empty-hunk +++ /dev/null (git deleted file mode, no hunks) unlinks. A hunked delete applies the minus lines first; leftover bytes rewrite the file (preview --diff). Path-only unlink is file.delete.
  • Use when: Patch replay needs to compose with earlier in-plan edits and share the same rollback or validation behavior.
  • Failure behavior: Merge conflicts without allow_conflicts exit 8 (CONFLICTS) with error_kind: "conflicts" under --json (not generic operation_failed / 9). Stale context without merge exits 5 (ambiguous). Stale minus lines on a hunked delete leave the file in place; regenerate minus lines from the current file, or use file.delete for path-only unlink.
  • Related: top level patch apply, patch merge

ast.rename

  • What it does: Renames all occurrences of an identifier within a file using tree-sitter AST awareness, skipping strings, comments, and documentation. References inside the renamed symbol and callers are updated atomically. Fields are path, old, and new (same names as replace / ast.replace). CLI: ast rename <path> --old <OLD> --new <NEW>.
  • Use when: You need a precise identifier rename that respects language semantics (e.g., renaming old_fn to new_fn without touching the string "old_fn" in a log message).
  • Failure behavior: Missing identifier exits 3 (no_matches) with the old name in the error (plan and CLI).
  • Related: replace (text-level), ast replace

ast.replace

  • What it does: Performs a scoped text replacement within a single symbol's body. Only the text inside the named symbol is searched and modified; the rest of the file is untouched.
  • Use when: You need to change a value, string, or expression inside a specific function or struct without affecting identically-named text in other symbols.
  • Failure behavior: Missing symbol exits 3 (no_matches) with error_kind: "no_matches".
  • Related: replace (file-level), ast.rename (identifier rename)

ast.rewrite_signature

  • What it does: Rewrites a function signature using tree-sitter. Structured fields visibility, parameters, and return_type map to FunctionSigEdit; optional new_signature replaces the whole signature span. Field old (alias name) is the function name. Library: api::ast_rewrite_signature. MCP: ast_rewrite_signature.
  • Body gap: High-level paths accept a logical new_signature without trailing whitespace and preserve the original gap before { (or insert a conventional space if the original was already glued). Trait/extern forms ending in ; do not get a spurious space. See #1503 / splice_function_signature.
  • Use when: Changing parameter lists, visibility, or return types without a brittle line scan (LLM agent hosts and embedders).
  • Failure behavior: Missing function name exits 3 (no_matches) with the function name in the error; JSON plans report error_kind: "no_matches".
  • Related: ast.replace, ast.rename

ast.insert

  • What it does: Inserts new source code at a position relative to an existing symbol (before, after, inside-start, inside-end). Handles indentation matching and blank-line separation.
  • Use when: You need to add a new function, field, or statement adjacent to or inside an existing symbol without manually computing line numbers.
  • Failure behavior: Missing container/adjacent symbol exits 3 (no_matches) with error_kind: "no_matches".
  • Related: ast.wrap, ast.group

ast.wrap

  • What it does: Wraps an existing symbol with a prefix and suffix, re-indenting the original body. Commonly used for wrapping a function in an impl block, a mod block, or an if guard.
  • Use when: You need to add structural nesting around an existing symbol (e.g., wrapping free functions in an impl, adding #[cfg(test)] module wrappers).
  • Failure behavior: Missing symbol or empty symbols list exits 3 / 1 (no_matches / invalid_input). Bad line-range numbers use invalid_input.
  • Related: ast.insert, ast.group

ast.imports

  • What it does: Adds or removes import statements from a file. Supports add and remove actions with deduplication. Language-aware: handles use (Rust), import (Python/JS/TS/Go/Java), #include (C/C++).
  • Use when: You need to manage imports programmatically after moving symbols, adding new dependencies, or cleaning up unused imports.
  • Related: ast.move, ast.extract_to_file

ast.reorder

  • What it does: Reorders top-level symbols (or symbols inside a scope) according to a strategy: alphabetical, reverse, kind-first (types before functions), or a custom ordered list of names. Preserves attached doc comments and attributes.
  • Use when: You want to enforce a consistent declaration order (e.g., alphabetical functions, types-first convention) or manually arrange symbols to match a specification.
  • Failure behavior: Missing container symbol exits 3 (no_matches). Malformed custom order items exit 1 with invalid_input.
  • Related: ast.group, ast.move

ast.group

  • What it does: Moves one or more symbols into a new or existing module block within the same file. Supports a preamble (e.g., use super::*;) and configurable placement (first-symbol position, end of file, or after a specific symbol).
  • Use when: You want to organize related symbols into a mod tests { ... } block or group utility functions into a sub-module without extracting to a separate file.
  • Failure behavior: Missing symbols exit 3 (no_matches) with error_kind: "no_matches".
  • Related: ast.extract_to_file, ast.move, ast.reorder

ast.move

  • What it does: Moves symbols from one file to another, removing them from the source and inserting at a specified position in the target. Supports creating the target file with an optional prepend. Preserves attached doc comments and attributes. Set update_imports with old_module_path and new_module_path to rewrite consumer use/import statements of the moved symbols (default off; missing module paths fail with invalid_input).
  • Use when: You need to relocate functions, structs, or constants between files during a refactoring (e.g., moving helpers from lib.rs to utils.rs).
  • Failure behavior: Missing source or target anchor symbols exit 3 (no_matches) with error_kind: "no_matches". update_imports: true without both module paths exits 1 with error_kind: "invalid_input".
  • Related: ast.extract_to_file, ast.group, ast.imports

ast.extract_to_file

  • What it does: Extracts a single symbol from a source file into a new target file. For module blocks, it can unwrap the module wrapper and un-indent the body. Leaves an optional replacement text (e.g., mod tests;) in the source. Supports a prepend for the target (e.g., use super::*;). Set update_imports with old_module_path and new_module_path to rewrite consumer use/import statements of the extracted symbol (default off; missing module paths fail with invalid_input).
  • Use when: You want to extract a test module, a large struct, or a helper block into its own file while leaving a mod declaration behind.
  • Failure behavior: Missing symbol exits 3 (no_matches) with error_kind: "no_matches". Existing target without force exits 1 with error_kind: "already_exists". update_imports: true without both module paths exits 1 with error_kind: "invalid_input".
  • Related: ast.split, ast.move, ast.imports

ast.split

  • What it does: Splits a file into multiple target files by distributing symbols. Each target specifies which symbols it receives and an optional prepend. Symbols not assigned to any target stay in the source (controlled by keep_in_source). Supports source_suffix and source_prefix for adding mod declarations. Enforces exhaustive accounting by default.
  • Use when: A file has grown too large and you want to distribute its symbols across several new files in one atomic operation, with mod re-exports generated automatically.
  • Failure behavior: Duplicate or unaccounted symbols exit 1 with error_kind: "invalid_input".
  • Related: ast.extract_to_file, ast.move, ast.group

Library API

  • What it does: Use patchloom as a Rust library (default-features = false, enable ast/mcp as needed). High level entry points in patchloom::api (search, replace_text, etc), plus execute_plan, make_plan, PathGuard for containment, and full plan types for tx. All public types are Send + Sync.
  • Use when: Embedding in LLM coding agents, custom tools, or tests without CLI spawn overhead. See cargo doc --no-default-features --features ast --open.
  • Notable: search_directory(root, pattern, opts) for parallel content search with globs/context (library equivalent of CLI search). Error paths and guards documented in api.rs.
  • Related: README "As a library", src/api.rs, src/lib.rs docs, examples/README.md entry for search_directory.

Embedder surfaces (LLM agent hosts)

NeedAPI
Fail-closed text replaceReplaceOptions.require_change + edit_error_kind / classify_error; prefer ReplaceOptions::for_agent() for shared agent primary+fallback policy (unique, require_change, fuzzy @ AGENT_MIN_FUZZY_SCORE 0.90, allow_absent_old: false, refuse_suspicious_fuzzy: true; #1965 / #2005). Approximate recovery stays a one-line override (allow_absent_old: true), not a second constructor (#1980). Custom options: refuse over-wide spans with api::fuzzy_span_suspicious / FuzzySpanPolicy (#1981); multi-op use op_honesty (#2006) or refuse_batch_if_suspicious_fuzzy (#2064)
Plan for_each without cliapi::expand_for_each / execute_plan (needs files; #2169). Expands before PathGuard. Zero-match is NoMatch. Do not combine with plan.cwd.
Plan format/validate shellRaw sh -c / cmd /C. MCP strips. api::lifecycle_cmds + api::refuse_lifecycle_shell_metas; execute_plan(..., Some(guard)) refuses metas as GuardRejected (#2168). true / cargo fmt / rustfmt still run.
Patch dest preflightapi::unquote_git_c_string / parse_diff_file_path / parse_diff_git_paths / patch_declared_paths (#2170–#2176). Do not quote-peel only or whitespace-split diff --git. parse_diff_git_paths accepts the full line or the pair after that prefix. Git copy creates dest and keeps source. Empty-create apply writes an empty dest and reports changed: true. Mixed binary/empty-create dests are listed; apply refuses unsupported git-meta.
Codex Begin Patchlooks_like_begin_patch / begin_patch_declared_paths / apply_patch / apply_patch_file / apply_begin_patch (#2219). Mixed Begin Patch + unified-diff is a typed error. Update hunks require a unique exact match. Do not copy a Begin Patch parser.
SEARCH/REPLACE unique applyparse_search_replace / apply_search_replace_blocks / apply_search_replace_document / looks_like_search_replace (#2220/#2221). apply_patch / CLI patch apply / MCP apply_patch detect the grammar. Default unique (multi-match is ambiguous, no write). replace_all: true or CLI --replace-all updates every exact match. Empty SEARCH is invalid input. Do not flip ReplaceOptions.unique on generic replace_text.
Non-anyhow error kindsclassify_error(&dyn Error) / classify_error_ref (#1659); EditErrorKind::FormatFailed for post-write hooks; EditErrorKind::TypeError for multi-doc / wrong-root doc navigation (CLI error_kind: type_error; #1883); EditErrorKind::AlreadyExists for create/rename dest-exists (#1947); EditErrorKind::NotFound / Conflicts / ChangesDetected for peels that previously collapsed to OperationFailed; EditErrorKind::Binary / InvalidEncoding for sole-path non-text loads (#1963)
CLI-stable error kind stringsapi::error_kind_str(&err) returns the same strings as CLI JSON (already_exists, not_found, guard_rejected, binary, invalid_encoding, …) without scraping Display (#1948); bool peels: is_already_exists, is_not_found, is_conflicts, is_changes_detected, is_type_error, is_format_failed, is_guard_rejected, is_invalid_input, is_binary, is_invalid_encoding, is_load_text_strict_fail (binary|encoding|invalid_input), is_no_match, is_ambiguous
Path binary preflightapi::is_binary_file / files::is_binary_file (8 KiB NUL probe; open fail → false; #1884 / #1910); writers still enforce binary on apply
Text I/O honestySole path: api::load_text / files::load_text_strict (binary → Binary / binary, invalid UTF-8 → InvalidEncoding / invalid_encoding; #1894 / #1910 / #1963); peels: api::is_binary / is_invalid_encoding / is_load_text_strict_fail; walks: try_read_text_file / SoftTextSkip / read_text_file (NotRegularFile / not_regular_file for FIFO/socket/dir; #2122); tx probe read_and_probe (content soft, IO hard); sole helper ops::file::sole_explicit_non_text; multi-path explicit_multi_path_non_text_refused (search/replace/tidy; specials not dropped via is_file); patch file+targets Strict (#1896)
Multi-doc library mergeapi::doc_merge(path, value, mode, guard, selector) with Some("0") / Some("[0]") for document 0; root object overlay on multi-doc is TypeError (#1909)
Document keys / lengthapi::doc_keys(path, selector) / api::doc_len(path, selector) (#2280). Empty or "." is the document root. Keys on an array (including multi-doc YAML root) is TypeError (keys on items[0] / len on items). Missing selector is NoMatch. Multi-match (items[*]) is Ambiguous and names items[0] / items[1]. Missing file (library/CLI/MCP doc_get/doc_has/doc_keys/doc_len / doc_query) is NotFound / not_found.
Line-oriented insertinsert_before / insert_after default (#1885): newline when payload looks like a new line or anchor is whole-line; mid-line bare stays byte-exact
Shell token renameReplaceOptions.command_position / ContentEdit::Replace (#1666)
Scoped symbol replace (literal/regex)ast_replace_in_symbol + AstReplaceInSymbolOptions.regex (#1658)
Project symbol discovery + multi-file renamefind_files_with_symbol then ast_rename_batch (#1664); one-shot ast_rename_project (#1689)
Match honesty (fuzzy confidence)EditResult / ContentEditsResult match_mode / match_score (#1662); CLI/MCP JSON (#1669); plan/tx TxChange + aggregate mode/score/match_count from engine meta (#1674)
Reject weak fuzzy matchesCLI --min-fuzzy-score / ReplaceOptions.min_fuzzy_score / plan min_fuzzy_score / MCP min_fuzzy_score (#1687); range 0.0..=1.0
Apply session id for surgical undoEditResult.backup_session after Apply (#1686); pair with restore_path_from_session
Session id on fail-restore / FormatFailedapi::backup_session_from_error(&err) (#2127); also format_failed_backup_session / MutationAfterBackupError
Alternate plan op after fail-closed write-navapi::suggested_op_from_error(&err) → plan serde name (doc.update, doc.delete_where) when a predicate/wildcard hit doc.set/ensure/delete (#2133); CLI/tx/MCP JSON field suggested_op (same values); keep error_kind: invalid_input
Nested monorepo backup listingbackup::list_sessions_under + ListSessionsOptions (#1688)
Ancestor backup root discoverybackup::find_backup_roots(path) walks parents for .patchloom/backups (#1934)
File op structured kindsfile_create / file_delete / file_rename / file_append: dest-exists without force → AlreadyExists (#1947); missing path → NotFound; dir/empty path → InvalidInput; append/prepend on binary → Binary / invalid UTF-8 → InvalidEncoding (#1963); path-only file_rename / file_delete succeed on binary and invalid UTF-8 with byte backup (#2031); force create overwrites non-text prior (#1962); PathGuard → GuardRejected (#1935)
AST signature rewrite kindsast_rewrite_signature: missing fn → NoMatch; binary → Binary (#1963); guard → GuardRejected (#1936)
In-memory multi-op with real diff headersapply_content_edits_with_label (#1665)
Surgical undo one pathbackup::restore_path_from_session(root, ts, path) (#1660)
Post-Apply format/lint + optional revertrun_post_write_validation / PostWriteHooks (#1663); also ReplaceOptions.post_write, WritePolicyOptions.post_write, AstRenameBatchOptions.post_write (#1690). Revert uses the file parent as backup root even when hooks cwd differs
Signature rewrite complete in one writeast_rewrite_signature body-gap invariant (#1661)

Shell command-position for embedders

When rewriting package managers or CLI tools in scripts (pipuv, wgetcurl), set command_position: true so only invocable tokens change:

# before
sudo pip install foo
uv pip list
pipenv run test

# after (command_position)
sudo uv install foo
uv pip list          # argument pip kept
pipenv run test      # longer token kept

Cannot combine with regex, whole_line, multiline, nth, insert-before/after, fuzzy, or context anchors (typed InvalidInput). Prefer this over word_boundary for shell files.

Patchloom: structured file edits for AI agents

AI coding agents are remarkably good at reasoning about code. They are remarkably bad at editing config files.

When an agent needs to bump a version in config.yaml, it reaches for sed or text replacement. That works until the regex strips a YAML comment, breaks indentation, or produces invalid syntax. When the task touches six files, that means six separate tool calls, each a full round-trip back to the LLM. And on Windows, sed and jq do not exist, so the agent falls back to verbose PowerShell or makes errors with unfamiliar syntax.

We built Patchloom to fix all three problems with a single Rust binary.

What it does

Patchloom edits JSON, YAML, and TOML files by selector path, not regex. It preserves comments and formatting because it parses the file instead of pattern-matching it. It batches multiple file edits into one tool call, cutting round-trips from six to one. And it works identically on Linux, macOS, and Windows with zero dependencies.

# Edit a YAML value by selector path; comments and formatting survive
patchloom doc set config.yaml database.port 5432 --apply

# Version bump across 6 files in a single tool call
patchloom batch --apply <<'EOF'
doc.set package.json version "2.0.0"
doc.set config.yaml app.version "2.0.0"
doc.set config.toml project.version "2.0.0"
replace README.md "1.0.0" "2.0.0"
replace CHANGELOG.md "1.0.0" "2.0.0"
file.create VERSION "2.0.0"
EOF

24 commands cover structured document editing, search and replace, markdown section editing, multi-file batching, atomic transactions with rollback, diff patching, file lifecycle operations, AST-aware code operations (rename, list, read, validate across 20 languages), operation schema export, and an MCP server that exposes everything as structured tool calls for MCP-capable agents.

The honest benchmark

We ran 11 real agent tasks, three times each, comparing Patchloom MCP, Patchloom CLI, and native editor tools. The agent was Claude Opus 4 via Grok Build.

Method      Total time (11 tasks)   Wins
──────────  ─────────────────────   ────
MCP mode    228.5s                  5/11
Native      233.8s                  3/11
CLI mode    321.9s                  0/11

MCP mode wins overall because structured tool calls skip shell syntax construction entirely. CLI mode is slowest because the agent must construct and quote shell commands for every call.

But Patchloom is not faster than native tools for everything. We are upfront about that. The agent instructions Patchloom generates include this table:

TaskUse Patchloom?Why
Edit JSON/YAML/TOML by selector pathYesParser-backed, comments preserved
Batch edits across multiple filesYesOne tool call instead of N
Append a row to a markdown tableYesHeading-aware, no line number guessing
Read a single fileNoNative read_file is faster
Simple text searchNoNative grep is faster
Single-file text replacementNoNative search_replace is faster

Patchloom tells agents when not to use it. The right tool for the right job.

Why it matters

Comments survive. doc set config.yaml database.port 5432 parses the YAML as a concrete syntax tree, changes the value at the selector path, and writes valid output. Inline comments, section comments, indentation, and key ordering are all preserved. A sed command cannot do this.

Round-trips disappear. Six file edits via native tools means six round-trips to the LLM. One batch call does the same work in a single round-trip. In our benchmarks, multi-file batch operations completed in under half the time of sequential native calls.

Failures roll back. tx plans group multiple operations with format and validate lifecycle hooks. Set strict: true and every file reverts if any step fails. No more partial edits to clean up after a broken CI run.

One binary everywhere. Same commands, same flags, same behavior on Linux, macOS, and Windows. No dependency on sed, jq, grep, or any Unix-specific tooling.

Self-documenting. Run patchloom agent-rules to generate an AGENTS.md file that teaches the agent exactly when and how to use each command. The agent reads it, learns the tool surface, and knows which tasks to handle natively and which to hand to Patchloom.

Try it

cargo install patchloom                    # crates.io
brew install patchloom/tap/patchloom       # macOS / Linux (Homebrew)
npx patchloom --version                    # npm / GitHub binary
# Windows (recommended)
scoop bucket add patchloom https://github.com/patchloom/scoop-bucket
scoop install patchloom/patchloom

Or download a prebuilt binary from GitHub Releases. winget (Patchloom.Patchloom) is published per release (use winget source update if search is stale). Chocolatey often lags GitHub while versions wait for moderation. Prefer Scoop or Releases when you need a known-current build.

Set up your project in one command:

patchloom init

This creates AGENTS.md in a new project or appends the rules to an existing agent instructions file, offers shell completions, and detects MCP configuration. If .vscode/ or .cursor/ exists, it prints ready-to-copy .vscode/mcp.json or .cursor/mcp.json snippets. Or generate just the agent instructions:

patchloom agent-rules >> AGENTS.md

For MCP mode (structured tool calls, no shell syntax):

cargo install patchloom
patchloom mcp-server

There is also a VS Code extension that handles binary detection, AGENTS.md generation, and MCP configuration from the command palette.

By the numbers

Numbers below track the current mainline product (not a frozen launch snapshot):

  • 4100+ tests, zero unsafe in library code (one unsafe killpg in exec.rs behind #[expect])
  • 24 commands including MCP server with 58 structured tool calls
  • Agent-tested with Grok 4.3, GPT-5.4, and Claude Opus 4.6
  • Cross-platform: Linux (x64, ARM64), macOS (x64, ARM64), Windows (x64, ARM64)
  • MIT OR Apache-2.0 licensed
  • Rust 1.95+, single static binary with no runtime dependencies

What comes next

Since launch, new capabilities have been added including line-oriented replace flags (--whole-line, --range, --collapse-blanks), project config defaults (.patchloom.toml), and expanded schema export. See the reference guide for the full command reference and current feature set.

We would love feedback on:

  • Which agent workflows hit friction that Patchloom could smooth
  • Missing operations or formats (.env? .ini? HCL?)
  • MCP integration with agents we have not tested yet
  • Performance reports from real-world projects

File issues, start discussions, or send PRs on GitHub.

Community launch pack (draft: do not post until a release ships)

Status: draft for maintainers. Post only after an explicit release tag / user-approved release PR merge. Do not publish stale version claims.

Demo script (temp dir)

REPO=$(git rev-parse --show-toplevel)
BIN="${REPO}/target/release/patchloom"
# or: BIN=$(which patchloom)
S=$(mktemp -d /tmp/patchloom-demo-XXXXXX)
cd "$S"
printf 'port: 1\n# keep me\n' > app.yaml
printf 'name: a\n---\nname: b\n' > stream.yaml

echo "=== dry-run (expect changes, no write) ==="
"$BIN" doc set app.yaml port 5432   # exit 2 typical without --apply
cat app.yaml

echo "=== apply structured YAML ==="
"$BIN" doc set app.yaml port 5432 --apply
cat app.yaml

echo "=== multi-doc selector ==="
"$BIN" doc set stream.yaml 0.name A --apply
"$BIN" doc get stream.yaml 0.name

echo "=== fail-closed fuzzy (exact old absent) ==="
printf 'const LIVE_NAME: i32 = 1;\n' > f.rs
"$BIN" --json replace LIVE_NAAME --new X --fuzzy --apply f.rs || true
grep LIVE_NAME f.rs

echo "=== agent-rules snippet ==="
"$BIN" agent-rules --mode mcp | head -40

echo "=== MCP explore (list_files in core pack since 0.24) ==="
# stdio MCP: use your host; CLI has no list_files subcommand
# Prefer MCP list_files / search_files over a second filesystem MCP

Post draft (Show HN / r/mcp)

Title options:

  • Show HN: Patchloom – structured file edits for AI agents (not another filesystem MCP)
  • Patchloom: dry-run, peels, and parser-backed JSON/YAML/TOML for agent tool loops

Body (plain text; no em dashes):

Patchloom is a single binary (CLI + MCP + Rust library) for agent-safe file edits.

Why not generic filesystem MCP / sed / yq alone?
- Dry-run by default (preview / exit 2 when changes would apply)
- Parser-backed JSON, YAML, TOML (comments, multi-doc honesty)
- Markdown section ops and tree-sitter AST renames
- batch/tx with undo; stable error_kind for hosts (binary, already_exists, …)
- Library hosts: ReplaceOptions::for_agent and fuzzy_span_suspicious
- MCP core pack includes list_files (ignore-aware inventory)

Install: https://patchloom.github.io/patchloom/
MCP Registry: io.github.patchloom/patchloom
Repo: https://github.com/patchloom/patchloom

Would love feedback from people wiring agents to config-heavy repos.

Channels (after release)

  • Hacker News Show HN
  • r/mcp
  • Optional: r/ClaudeAI / r/ClaudeCode if rules allow tooling posts
  • Update Glama listing description if manual form needed
  • Comparison docs, README positioning, directory audit (competitive research batch)