atomwrite 0.1.38

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
docs.rs failed to build atomwrite-0.1.38
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: atomwrite-0.1.36

Read in Portuguese / Leia em Português

atomwrite

  • Atomic file operations for LLM agents: one CLI, all-or-nothing writes

docs.rs Crates.io License MSRV Downloads Rust

What Is It

  • A single Rust binary that handles every file operation an LLM agent needs
  • 47 subcommands (SSOT: clap Commands / agent-surface), all listed with an example in Commands
  • Every write is atomic: tempfile, fsync, rename, fsync directory
  • Every response is NDJSON: one JSON object per line, machine-readable by default
  • Every file gets a BLAKE3 checksum: detect drift, verify integrity, enable optimistic locking

Installation

From crates.io

cargo install atomwrite

Slim and full builds

  • Features: core, ast, lang-rust, lang-ts, lang-py, lang-go, lang-full, watch, full (default = core + ast + lang-rust + lang-ts + lang-py; watch is opt-in; semantic-search is always compiled)
  • Slim build (core only, 26,560,352 bytes measured on 2026-11-04, DERIVADO 26560352 / 1048576 = 25.33 MiB, above the local size-gate of 15 MiB): cargo install --path . --locked --force --no-default-features --features core
  • Full/default build (AST languages, 71,084,440 bytes measured on 2026-11-04, DERIVADO 71084440 / 1048576 = 67.79 MiB): cargo install --path . --locked --force
  • Build with watch: cargo install --path . --locked --force --features watch (the default binary exits 78 on watch with a rebuild hint)
  • Slim build: outline and sparse outline exit 78 with a rebuild hint (measured), and query, scope, transform and write --syntax-check have stubs for the same case (read from src/commands/*_stub.rs and src/syntax_check_stub.rs, the slim build was not executed to confirm)
  • cargo xtask check-features checks the core and core,watch sets
  • The features i18n-cjk, i18n-rtl, i18n-europe, i18n-full and slow-tests are placeholders or test switches: do not enable them
  • The 5-8 MB target of the product plan applies to the core profile, not to the default AST build: the measured core build (25.33 MiB) is above that target and above the 15 MiB gate

From source

git clone https://github.com/danilo-aguiar-br/atomwrite.git
cd atomwrite
cargo build --release

Shell completions

# Bash
atomwrite completions bash > ~/.local/share/bash-completion/completions/atomwrite

# Zsh
atomwrite completions zsh > ~/.zfunc/_atomwrite

# Fish
atomwrite completions fish > ~/.config/fish/completions/atomwrite.fish

Quick Start

# Write a file atomically from stdin
echo "hello world" | atomwrite --workspace . write src/hello.txt

# Read it back with checksum
atomwrite --workspace . read src/hello.txt

# Search across a directory
atomwrite --workspace . search 'hello' src/

# Replace text with atomic writes
atomwrite --workspace . replace 'hello' 'world' src/

# Evaluate math and unit conversions
atomwrite calc "2 hours + 30 minutes to seconds"

# Set a value in a TOML file (preserves comments)
atomwrite --workspace . set Cargo.toml package.version 0.2.0

# Walk the AST of a Rust file (downloads the parser on first use, see Network And Privacy)
atomwrite --workspace . query src/main.rs --kinds

# Extract the outline of a Python file
atomwrite --workspace . outline src/app.py

# Real tree-sitter syntax check before writing (exit 88 on a syntax error)
echo "fn broken(" | atomwrite --workspace . write --syntax-check src/x.rs

# Report stale WAL sidecars and estimate the reclaim
atomwrite --workspace . wal-stats

# Reap terminal journals older than the threshold
atomwrite --workspace . wal-heal . --threshold-secs 3600

What Is New In v0.1.38 (2026-11-04)

  • read --format raw stops at 24,000 bytes and prints one stderr line with the next --lines, because the Bash tool already cut the stream at 30,000 characters without saying so
  • --all removes the cap, and the raw path streams with BufReader instead of holding the file in memory
  • --number prints N:text, --lines is repeatable with --range as an alias, and several windows and several files arrive in one call with ==> path <== headers
  • --symbol NAME and --line N --enclosing select by syntax tree, and --no-comments drops comment and blank lines before the window is counted
  • search --format text writes one path:line:col:text per match under a byte cap, and a tree where every file was binary exits 89 ALL_SKIPPED_BINARY instead of NO_MATCHES
  • list --format text writes one path per line, and total_bytes counts files only
  • outline --format tsv writes line<TAB>kind<TAB>name with the path once, lists Markdown headings and setext sections, parses raw bytes, and publishes parse_errors with first_error_line
  • outline reads a Markdown heading as a section that runs to the end of its own section, and read --symbol Part returns that whole section with its sub-sections
  • apply matches whole lines, refuses a repeated SEARCH with exit 65 MATCH_AMBIGUOUS unless --all is given, and refuses a block that lost ======= or >>>>>>> REPLACE by naming its index
  • apply --show returns the changed lines with their 1-based numbers, which removes the re-read that confirmed the swap
  • apply runs the syntax check before and after the swap and refuses only when the parse-error count rises, with --no-syntax-check as the opt-out
  • apply --format unified accepts @@ ... @@ and a bare @@, so a hunk without numbers matches by context and more than one match is refused with the line of each one
  • diff reports 1-based line plus old_line and new_line, and --select is honored on the terminal summary
  • delete publishes backups_kept, backup_bytes and dirs_left when a backup or a leftover exists
  • The failure envelope carries ok, code and exit, and error: true left it because it duplicated ok: false plus code, cost 13 bytes on every failure (179 to 166 bytes on the same path) and no caller branched on it alone
  • The lock targets the inode with std::fs::File::try_lock, and delete takes the same lock before it hashes and unlinks
  • delete -r <dir> owns the whole named tree, including hidden, ignored, linked, fifo, socket and device entries, and a link is never followed
  • target_resolved is always an array in invocation order, because a path may contain the comma that used to join several targets
  • read --format raw on a single target honors --max-filesize and exits 65 FILE_TOO_LARGE above the ceiling
  • A file whose image signature is damaged fails read with exit 65 INVALID_INPUT instead of returning its bytes as text
  • output.profile = "lean" drops the digests, the byte counts and the timing from a successful mutation record, which took one apply on a three-line file from 433 bytes to 196
  • config lists 43 keys, and atomwrite commands --brief prints one line per subcommand in 5 020 bytes against 67 492 for the full tree
  • Pin agents to ^0.1.38. 47 subcommands (SSOT)

Read Expansion In v0.1.37 (2026-10-31)

  • read takes one or more paths: explicit paths are read in argv order, and an operand carrying *, ? or [ is a pattern the binary expands itself against one level of its own directory
  • Pattern order is the LC_ALL=C shell order, taken from the encoded bytes of each name, so one code path sorts on all three targets, and only Linux was executed, while macOS and Windows stay unvalidated
  • --format raw concatenates every target with no header, no separator and no newline of its own, so atomwrite read --format raw '*.rs' | cmp - <(LC_ALL=C cat *.rs) exits 0 with an empty diff
  • The batch copies in 64 KiB blocks, so memory stays flat instead of holding the sum of every file, and --timeout-secs still cuts the copy with exit 124 because every block advances the cancel check
  • A target that fails prints one line on stderr and the run continues with the next one, and the final status is the semantic code of the first failure: 4 for a missing path, 13 for a permission, 126 outside the --workspace
  • read --format raw errors now go to stderr and never to stdout, because that channel promised bytes and a JSON envelope there was the defect
  • --lines, --line, --context, --head, --tail, --grep, --stat and --verify-checksum need a single target: naming several exits 65 with a message that names the flag
  • ** is out of scope: a pattern with ** or with more than one separator exits 65 instead of matching something else in silence
  • The default NDJSON format is unchanged, and one target in --format raw is byte-identical to 0.1.37
  • [profile.dev] opt-level = 1 cut the dev-profile fuzzy cost from 18533 ms to 4232 ms on the same 15 MB corpus
  • Pin agents to ^0.1.37. 47 subcommands (SSOT)

What Is New In v0.1.37 (2026-10-31)

  • Risk assessment now runs in every verb that writes: edit, edit-loop, replace, case, transform and codemod publish risk_assessment when the payload matches a content-risk pattern, exactly as write already did. The size barriers stay out of these verbs on purpose, because they rewrite a region and a shorter result is the operation working
  • A grammar this host cannot provide is exit 69 GRAMMAR_UNAVAILABLE with class precondition_failed, never 255 INTERNAL_ERROR. atomwrite doctor reports the new grammar_cache check with how many of the 23 grammars the AST verbs can need are already cached
  • write.content_risk_patterns is reachable with config get and config set as a list value, so the risk patterns are XDG configuration instead of a compiled literal (43 keys)
  • log.file and log.rotation are accepted in the workspace scope too, and the [log] section of .atomwrite.toml now starts the log sink instead of being validated and discarded
  • batch --transaction publishes one verdict: the rollback event carries ok: false with the code and exit of the failure that caused it, and the transaction locks every target in path order before the snapshot
  • --version no longer appends -dirty, so an untracked file cannot change the string

What Is New In v0.1.37 (2026-10-15 draft)

  • Full history lives in CHANGELOG.md, the upgrade steps live in docs/MIGRATION.md
  • New config verb (the 47th subcommand): config path|list|get|set|unset|doctor reads and writes 37 persistent tuning keys, see Configuration
  • New extract-artifact verb: bounded signal from PDF, OOXML and image headers, never raw bytes
  • Global stream shaping: --sort-by, --dedupe, --aggregate, --join, --diff-against, --redact, --normalize-encoding, --truncate-content, --max-output-bytes, see Global Flags
  • The terminal success event carries ok: true (the per-item events of a stream, such as entry and match, do not), and a failing run ends in exactly one terminal event with ok: false
  • Verbs that write take an explicit path (exit 2 without one), and the terminal event carries target_resolved and target_source: "argv"
  • Every terminal event also carries workspace_source: argv when the root came from --workspace, and cwd when it came from the process working directory
  • edit-loop is all-or-nothing: a pair with no match exits 65 and writes nothing, while --partial writes the matches and exits 87; the checksum it reads under the lock is the write base, so a concurrent edit exits 82 STATE_DRIFT
  • Atomic writes take an automatic advisory lock (waits up to 5 s, exit 83), and search, write, edit and the other stdin verbs obey SIGTERM, SIGHUP and the deadline (exit 143, 129, 124)
  • watch left the default feature set, and --version prints atomwrite 0.1.37 (<sha>) <target triple> inside a tracked checkout and atomwrite 0.1.37 (no-git) <target triple> outside one
  • Changes that can break a consumer:
    • top-level JSON fields whose value was null are no longer emitted: test for an absent key, not for null
    • --max-output-bytes now requires --allow-truncate, and each flag alone exits 2
    • semantic-merge with a conflict exits 84 (MERGE_CONFLICT), and apply with a unified diff that does not match the file exits 65 and writes nothing
    • an invalid regex or glob exits 65, not 255, and get, set and del on an invalid TOML or JSON file exit 65
    • case, transform, scope and codemod can exit 82 (STATE_DRIFT) when a file changed during the run
    • sparse list applies --include and --exclude, and search.max_inflight_bytes starts at 4 MiB
    • the Rust library API changed, see Library API
  • Pin agents to ^0.1.37. 47 subcommands (SSOT). CLI + XDG only (no product ATOMWRITE_* knobs; no product telemetry). Error envelopes always emit, with two deliberate exceptions: a broken pipe and a checksum divergence already published as one hash_verify event

Why

  • LLM agents juggle dozens of shell commands to manipulate files
  • A crash mid-write can corrupt a file written in place
  • Parsing unstructured CLI output wastes tokens and causes hallucinations
  • Agents need checksums to detect concurrent edits but rarely compute them
  • atomwrite solves all four problems with one cargo install

Objectives

  • Spend fewer tokens: one NDJSON object per call replaces the shell pipeline, and commands --brief answers in 5 020 bytes where the full tree costs 67 492
  • Lower the financial cost, because the bill follows the tokens and the round trips, and one call replaces the several shell calls a file task needs
  • Reduce errors with an atomic write and a BLAKE3 checksum on every response, so a stale or partial result is caught instead of assumed
  • Reduce hallucinations with a type discriminator, a closed schema per verb and an exit code that names the failure
  • Reduce the time spent on each task, because the read, the search and the write run in one process instead of a chain of shell tools
  • Reduce latency with local execution and no network on the file path, measured at medians of 5.6 ms on read and 6.7 ms on write for small files
  • Multiply the speed of operations with batch, edit-loop, replace, case, transform and codemod settling many files in one call
  • Cut the lead time between the request and the verified result, because the response already carries the checksum, the target_resolved and the next step
  • Make the agent faster, more effective and more assertive, since the exit code, the code and the suggestion say what to do next without a retry guess

Superpowers

Atomic Writes

  • Uses tempfile + fsync + rename + directory fsync on every write
  • A reader sees the old file or the new file, never a half-written one
  • Durability is set by write --durability full|fast|auto (default auto); a power cut was not simulated in the tests
  • Optional real syntax check via tree-sitter (--syntax-check flag on write, exit 88)
  • Optional WAL sidecar for crash recovery (.atomwrite.journal.<target>.atomwrite.journal.json)

NDJSON Output

  • stdout is ALWAYS structured JSON, one object per line
  • Every object carries a "type" discriminator field
  • Agents parse output without regex or brittle text scraping
  • Errors also emit JSON on stdout with ok: false plus code, exit, message and suggestion

BLAKE3 Checksums

  • Every read and write response includes a BLAKE3 hash
  • Use --expect-checksum for optimistic locking on concurrent edits
  • Detect state drift before applying changes

Parallel Search

  • Built on the ripgrep engine for file content search
  • Respects .gitignore automatically
  • Returns structured matches with file, line, column, and context

AST-Aware Transforms

  • transform and codemod rewrite by syntax tree through ast-grep 0.43, which supports 28 languages (measured: each of the 28 names is accepted, zig and erlang exit 65 unsupported language)
  • query, outline and --syntax-check use tree-sitter-language-pack 1.22.1, whose manifest lists 372 languages (measured on 2026-10-31 against the manifest of that version)
  • Automatic language detection by file extension covers 23 languages (src/syntax_check.rs); --language NAME accepts a parser that is already in the local cache
  • Refactor code by syntax tree, not fragile regex

Grammatical Scoping

  • Select AST categories like comments, functions, classes, and strings
  • Apply actions: delete, uppercase, lowercase, titlecase, squeeze, or replace
  • Covers Rust, Python, JavaScript, TypeScript, and Go with prepared queries
  • Use --pattern for custom AST patterns beyond the built-in queries

Batch Operations

  • Execute write, replace, delete, edit, hash, move, and copy operations from an NDJSON manifest
  • Use --transaction for all-or-nothing execution with automatic rollback
  • All operations in a batch share the same atomic guarantees

Structured Config Editing

  • set writes a value at a dotted path in TOML or JSON files (preserves comments via toml_edit)
  • get reads a value at a dotted path with auto-detected format
  • del removes a key (with --force-missing to treat missing keys as no-op success)
  • case renames identifiers inside files via heck (snake, camel, pascal, kebab, screaming-snake)

Usage

  • All output goes to stdout as NDJSON
  • Logs go to stderr: warnings and errors always, info and debug with -v / -vv
  • Always pass --workspace <DIR> to restrict operations to a project root
  • Use --dry-run before destructive operations
  • Use --expect-checksum <HASH> for optimistic locking
  • Use --locale <TAG> or atomwrite locale --set / locale --clear for UI locale (en, pt-BR); NDJSON codes stay English
  • Configuration is CLI flags + XDG / .atomwrite.toml only, see Configuration
  • Pipe stdin for write and batch commands
  • Agent-native stdout globals (all subcommands): --select / --fields (CSV top-level keys), --filter KEY=VAL (repeatable AND), --limit N (long-only: there is no global -n, and replace owns short -n)
  • Error NDJSON always has ok: false and usually match_count: parse exit / code first; success shaping via --filter/--limit never drops error envelopes

Global Flags

  • 33 flags are accepted before or after the subcommand (source: atomwrite commands --include-globals); a hidden --json is also accepted and ignored because the output is always NDJSON
  • The short help -h of a subcommand lists only that verb's own flags, and its footer line points to atomwrite --help for the 33 globals
  • Aliases: --max-concurrency for -j / --threads, --timeout for --timeout-secs, --fields for --select
  • Flags named like a global but local to one command (for example search --max-filesize, 10 MiB by default) are listed in atomwrite <command> --help
Flag Default What it does
-v, --verbose off raises log verbosity on stderr (-v info, -vv debug, -vvv trace)
-q, --quiet off lowers it (-q error, -qq off)
--workspace <DIR> current directory root of the path jail, always pass it
--config <PATH> none explicit .atomwrite.toml, fails if missing or malformed; replaces both files in config get and config list
--color <WHEN> auto auto, always or never
--no-color off disables colored output
--ready-file <PATH> none writes the PID after the signal handlers are installed (test harness only)
--no-gitignore off does not respect .gitignore files
--hidden off includes hidden files and directories
--follow-symlinks off follows symbolic links
-j, --threads <N> automatic worker bound; omitted or 0 = the smaller of logical CPUs, 16 and the RAM cap; an explicit N is honoured, also above 16
--max-filesize <BYTES> 1 GiB rejects a larger file (read, edit, hash and write exit 65 FILE_TOO_LARGE), and on write it also bounds the stdin payload, so the flag names one ceiling and the payload and the target obey the same one; only search skips silently, with its own local 10 MiB limit counted in files_skipped
--timeout-secs <N> 120 global deadline, 0 disables it, exit 124 with a cancelled event that has deadline_exceeded: true
--no-progress off disables NDJSON progress events on long operations
--json-schema off emits the JSON Schema of the command output and exits
--print-schema [NAME] none lists the 55 schema names, or emits one schema; an unknown name exits 65
--non-interactive off declares agent-hosted execution: never prompts, never reads a confirmation from stdin
--confirm-side-effect off acknowledges a side effect where a command contract asks for it; on a verb that writes nothing it is rejected with exit 65 INVALID_INPUT instead of being a silent no-op
--allow-truncate off opts in to value truncation, required by --max-output-bytes
--locale <TAG> resolved en or pt-BR; precedence: this flag, the saved preference, the OS language, en
--no-auto-heal off skips the startup wal-heal pass (3600 s threshold, 100 ms budget)
--select <KEYS> all keys keeps only the listed top-level keys (CSV)
--filter <KEY=VALUE> none keeps events whose key equals the value (!= and == work; repeatable, AND); never drops an error envelope
--limit <N> none work budget: the target list is cut before the side effect and the walk stops at the record that reaches the cap, so --limit 1 touches exactly one target and a huge tree is not read to the end; and the terminal summary carries walker_stopped_early: true only when the walk was actually cut before the tree ended
--sort-by <KEY> arrival order sorts events by path-line, line-path or any key (buffers until the end)
--dedupe <KEYS> off drops events whose key tuple (CSV) was already seen
--aggregate <KEY[:OP[:FIELD]]> off groups buffered events by KEY; OP is count, sum, min or max
--join <KEY:SUFFIX> off reads the NDJSON table from stdin once (up to 16 MiB) and attaches the matching rows as a SUFFIX array to every data event that has KEY; events without KEY or without a match, failure events and the summary stay as they were; runs before --select, --filter and --limit (with --sort-by, --dedupe or --aggregate: after the sort, before the dedupe); exits 65 INVALID_INPUT for a TTY stdin, a table above 16 MiB, a line that is not a JSON object, a bad format, a KEY with no row holding a string value, or a command that already reads stdin (write, edit, edit-loop, apply, extract, batch without --file, hash --stdin, calc or regex reading stdin, diff with -)
--truncate-content <KEY:LEN> off cuts one string key to LEN characters
--redact off replaces secret-shaped values with [redacted]
--normalize-encoding off normalizes every string value to Unicode NFC
--diff-against <PATH> off compares the events against a baseline NDJSON file inside the jail and emits one stream_diff event
--max-output-bytes <N> off cuts every string value, checksum included; needs --allow-truncate and each flag alone exits 2
# Stream shaping examples
atomwrite --workspace . --sort-by path-line search 'TODO' src/
atomwrite --workspace . --aggregate type:count list src/ --depth 1
atomwrite --workspace . --dedupe kind list src/ --depth 1
atomwrite --workspace . --truncate-content content:20 read src/main.rs
atomwrite --workspace . --redact read src/main.rs
atomwrite --workspace . --normalize-encoding read src/main.rs
atomwrite --workspace . list src/ --depth 1 > base.ndjson
atomwrite --workspace . --diff-against base.ndjson list src/ --depth 1
atomwrite --workspace . --max-output-bytes 16 --allow-truncate --select path,checksum read src/lib.rs
atomwrite --workspace . --non-interactive read src/lib.rs
printf '{"path":"src/lib.rs","owner":"alice"}\n' | atomwrite --workspace . --join path:meta list src/ --depth 1

Commands (47 total — SSOT)

Core I/O

  • read — read files with metadata, checksum and a bounded body: --format raw or ndjson, --head, --tail, --lines and --range (repeatable), --number, --symbol NAME, --line N --enclosing, --no-comments, --all, and --pages, --max-tokens, --crop or --ocr for a PDF or a picture
  • write — create or overwrite files atomically via stdin (large existing targets need --ack-overwrite)
  • edit — surgically edit by line number, text marker, or exact match
  • edit-loop — apply N old/new pairs from NDJSON stdin in one atomic write
  • delete — delete files, copying one only when the call asks (--backup or [defaults].backup = true; --plan plan-only; --confirm/--yes rejected)
  • copy — copy files with checksum verification
  • move — move or rename files atomically (EXDEV copy-fallback)
  • apply — apply patches from stdin (unified diff, search/replace, full, markdown); the target is optional so one call patches several files, --show returns the changed lines with their numbers, and --no-syntax-check opts out of the pre/post parse
  • scratch — create agent staging dir under workspace (never OS /tmp)
  • verify — verify file integrity by comparing BLAKE3 checksum

Search and replace

  • search — search file contents in parallel (ripgrep engine); --format text prints one path:line:col:text per match under a byte cap, -e/--regex repeats a pattern and search.format makes the text format the default
  • replace — replace text across files with atomic writes
  • transform — AST refactoring via ast-grep (28 languages)
  • codemod — multi-rule codemod campaign (YAML rules, delegates to transform)
  • semantic-search — offline token Jaccard search (no network, no embeddings API)
  • semantic-merge — three-way line-based merge (not AST/embedding) for multi-agent writes

Inspection

  • hash — calculate BLAKE3 checksums
  • stat — metadata alias for read --stat (size, mode, checksum; no body)
  • count — count lines, matches, or files by extension
  • diff — compare two files or a file against stdin (unified or stat), reporting a 1-based line plus the separate old_line and new_line
  • list — list the project file structure with metadata (one NDJSON record per entry, or one path per line with --format text, where total_bytes counts files only)
  • sparse — budgeted sparse list/read/outline (cap files and bytes for agent context)
  • extract — extract fields from NDJSON input via pipe (positional fields or local --field; never local extract --select: global --select only projects output keys)
  • extract-artifact — bounded signal from PDF, OOXML and image headers (never raw artifact bytes)
  • scope — grammatical scoping (delete all comments, etc.)
  • regex — generate regex from examples
  • calc — math and unit conversions
  • doctor — diagnose environment and dependencies
  • locale — show resolved UI locale / persist preference (XDG)
  • commands — emit full command tree as JSON for agent discovery, and --brief prints one line per subcommand
  • config — persistent tuning keys: config path|list|get|set|unset|doctor, 43 keys, --scope global|workspace required on set and unset (and on path)
  • agent-surface — agent tool surface manifest (integration by subprocess, CLI with NDJSON output)
  • watch — watch filesystem events (needs a build with --features watch; the default binary exits 78)
  • completions — generate shell completions (bash, zsh, fish, elvish, powershell)

Backup and recovery

  • backup — create timestamped backups with BLAKE3 checksums
  • rollback — restore from a previous backup
  • prune-backups — prune .bak.YYYYMMDD_HHMMSS backups by age or count
  • batch — NDJSON-driven batch operations (transactional)
  • recipe — named multi-step recipes (list / run)

Structured config editors

  • set <PATH> <KEY_PATH> <VALUE> — write a value at a dotted path in a TOML or JSON file, preserving comments and key order via toml_edit
  • get <PATH> <KEY_PATH> — read a value at a dotted path, NDJSON {"type":"get","key_path","value","found","format"}
  • del <PATH> <KEY_PATH> — remove a key, --force-missing treats a missing key as a no-op success
  • case <FILES...> --subvert OLD NEW --to <style> — rename identifiers inside the given FILES via heck (styles: snake, camel, pascal, kebab, screaming-snake); a directory argument expands through the shared walker and honours --include and --exclude, and a directory that yields no file exits 65 instead of succeeding with nothing renamed

AST tools (via tree-sitter-language-pack)

  • query <PATH> [--kinds|--query <KIND>|-Q <KIND>|--tree] [--positions] — walk a tree-sitter AST and emit nodes as NDJSON
  • outline <PATH> [--kind <KIND>] [--positions] [--format tsv] — extract high-level structure (functions, classes, structs, enums, traits, modules, Markdown headings) as NDJSON or as line<TAB>kind<TAB>name; needs a file, not a directory, and publishes parse_errors with first_error_line
  • write --syntax-check — real syntax check via tree-sitter, exit 88 with first error line, column, kind and message

WAL cleanup

  • wal-stats — telemetry snapshot: total journals, by-state breakdown, oldest age, total size, by-directory breakdown, auto-heal recommendation, estimated reclaim bytes
  • wal-heal <PATH> [--threshold-secs N] [--max-duration-ms N] — reap stale terminal (committed or aborted) journals older than the threshold, and also remove a Started journal whose owning PID no longer exists, reported as dead_owner_removed

Common patterns for each subcommand

# read (bounded at read.max_output_bytes; --all removes the cap)
atomwrite --workspace . read src/main.rs
atomwrite --workspace . read --head 20 src/main.rs
atomwrite --workspace . read --format raw --lines 1:3 src/main.rs
atomwrite --workspace . read src/main.rs --number --lines 1:20
atomwrite --workspace . read src/main.rs --symbol main
atomwrite --workspace . read src/main.rs --line 42 --enclosing
atomwrite --workspace . read --format raw --lines 1:5 --lines 40:60 src/main.rs
atomwrite --workspace . read src/main.rs --grep 'unimplemented' --no-comments

# write
echo "fn main() {}" | atomwrite --workspace . write src/main.rs

# edit
atomwrite --workspace . edit src/main.rs --old "old text" --new "new text"

# edit-loop (N old/new pairs, one atomic write)
printf '%s\n' '{"old":"foo","new":"bar"}' '{"old":"baz","new":"qux"}' \
  | atomwrite --workspace . edit-loop src/version.rs

# scratch: stage under .atomwrite/scratch/, never OS /tmp outside --workspace, for --old-file/--new-file
atomwrite --workspace . scratch init .atomwrite/scratch
atomwrite --workspace . edit src/main.rs \
  --old-file .atomwrite/scratch/old.txt \
  --new-file .atomwrite/scratch/new.txt

# verify
atomwrite --workspace . verify src/main.rs abc123def456

# search (search.format = text makes the cheap shape the default)
atomwrite --workspace . search 'TODO' src/ --include '*.rs'
atomwrite --workspace . search 'TODO' src/ --format text --limit 20
atomwrite --workspace . search -e 'TODO' -e 'FIXME' src/ --format text

# replace
atomwrite --workspace . replace 'old_name' 'new_name' src/ --include '*.rs'

# hash
atomwrite --workspace . hash src/main.rs src/lib.rs

# delete
atomwrite --workspace . delete src/temp.rs --backup
atomwrite --workspace . delete --plan src/temp.rs

# count
atomwrite --workspace . count src/ --by-extension

# diff
atomwrite --workspace . diff src/old.rs src/new.rs --unified

# move
atomwrite --workspace . move src/old.rs src/new.rs

# copy
atomwrite --workspace . copy src/template.rs src/new_module.rs

# list
atomwrite --workspace . list src/ --depth 2
atomwrite --workspace . list src/ --format text --depth 2

# extract: positional fields or local --field; NEVER local extract --select
atomwrite --workspace . search 'TODO' src/ | atomwrite extract path line
atomwrite --workspace . search 'TODO' src/ | atomwrite extract --field path --field line
# thin agent shape (global --select projects output keys only):
atomwrite --workspace . search 'TODO' src/ --select path,line --limit 20

# extract-artifact (PDF, OOXML or image header; an image here)
atomwrite --workspace . extract-artifact logo.png

# calc
atomwrite calc "2 GiB to bytes"
atomwrite calc "sqrt(144) + 3^2"

# regex
atomwrite regex "2024-01-15" "2025-12-31" "2026-06-01"

# transform
atomwrite --workspace . transform -p 'println!($$$ARGS)' -r 'tracing::info!($$$ARGS)' -l rust src/

# scope
atomwrite --workspace . scope src/ --lang rust --query comments --delete --dry-run

# batch
cat manifest.ndjson | atomwrite --workspace . batch --transaction

# backup
atomwrite --workspace . backup src/config.toml

# rollback
atomwrite --workspace . rollback src/config.toml

# prune-backups
atomwrite --workspace . prune-backups --max-age-secs 86400 src/config.toml
atomwrite --workspace . prune-backups --max-count 3 --dry-run src/config.toml

# apply: the target may come from the patch, so one call patches several files
echo "new content" | atomwrite --workspace . apply src/file.txt --format full
printf '<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE\n' | atomwrite --workspace . apply --show src/file.txt

# set
atomwrite --workspace . set Cargo.toml package.version 0.2.0

# get
atomwrite --workspace . get Cargo.toml package.version

# del
atomwrite --workspace . del Cargo.toml package.metadata.boring

# case: a file or a directory (the directory is expanded through the walker)
atomwrite --workspace . case src/main.rs --subvert user_id UserId --to pascal

# query
atomwrite --workspace . query src/main.rs --kinds
atomwrite --workspace . query src/main.rs -Q function_item --positions

# outline
atomwrite --workspace . outline src/app.py
atomwrite --workspace . outline src/app.py --format tsv

# wal-stats
atomwrite --workspace . wal-stats

# wal-heal
atomwrite --workspace . wal-heal . --threshold-secs 3600 --max-duration-ms 100

# stat
atomwrite --workspace . stat src/lib.rs

# sparse
atomwrite --workspace . sparse list --max-files 100 --max-bytes 1048576 src/

# semantic-merge (line-based, not AST; exit 84 when there are conflicts)
atomwrite --workspace . semantic-merge \
  --base base.rs --ours ours.rs --theirs theirs.rs --output src/merged.rs

# recipe
atomwrite --workspace . recipe list
atomwrite --workspace . recipe run --name search-replace-verify \
  --path src --pattern unwrap --replacement expect --fuzzy auto --dry-run

# agent-surface
atomwrite --workspace . agent-surface

# watch (needs a build with --features watch; the default binary exits 78)
atomwrite --workspace . watch . --debounce-ms 200 --max-events 20

# codemod
atomwrite --workspace . codemod --rules rules.yaml --dry-run src/

# semantic-search
atomwrite --workspace . semantic-search "fn main println" src/ --k 10

# doctor
atomwrite --workspace . doctor
atomwrite --workspace . doctor --strict

# locale
atomwrite locale
atomwrite locale --set pt-BR
atomwrite locale --clear

# commands (full clap tree for agent discovery; --brief fits one screen)
atomwrite commands
atomwrite commands --include-globals
atomwrite commands --brief

# completions
atomwrite completions bash

# config (persistent tuning keys, see Configuration)
atomwrite --workspace . config list
atomwrite --workspace . config get fuzzy.threshold
atomwrite --workspace . config set fuzzy.threshold 0.9 --scope workspace
atomwrite --workspace . config unset fuzzy.threshold --scope workspace
  • codemod --rules reads a YAML list of items with language, pattern, rewrite and an optional id; the ast-grep style rule: and fix: form exits 65
  • prune-backups --dry-run takes no value: omit the flag to apply the pruning

Local Flags Per Command

  • Each command accepts the local flags listed here on top of the 33 global flags in the Global Flags section
  • agent-surface: --format
  • apply: --strict-atomic, --format, --backup, --no-backup, --keep-backup, --retention, --dry-run, --ack-overwrite, --allow-shrink, --show, --all, --no-syntax-check
  • backup: --output-dir, --retention, --dry-run
  • batch: --strict-atomic, --dry-run, --file, --transaction, --input-schema, --batch-size, --backup, --no-backup, --keep-backup, --retention
  • calc: --stdin
  • case: --strict-atomic, -g/--include, --exclude, --subvert, --to, --backup, --no-backup, --keep-backup, --retention, --preserve-timestamps, --dry-run
  • codemod: --strict-atomic, --rules, --dry-run, --backup, --no-backup, --keep-backup, --retention
  • commands: --include-globals, --brief
  • completions: --install
  • copy: --strict-atomic, --backup, --no-backup, --keep-backup, --retention, -f/--force, -r/--recursive, --preserve, --dry-run, --no-reflink, --preserve-xattr
  • count: --by-extension, --by-size, --top, -g/--include, --exclude
  • del: --strict-atomic, --dry-run, --backup, --no-backup, --keep-backup, --retention, --preserve-timestamps, --force-missing
  • delete: --backup, --no-backup, --keep-backup, --retention, -r/--recursive, -g/--include, --exclude, --older-than, --plan, --dry-run, --confirm, -y/--yes
  • diff: --unified, --stat, -C/--context, --algorithm
  • doctor: --strict
  • edit: --after-line, --before-line, --range, --delete-range, --after-match, --before-match, --between, --old, --new, --old-file, --new-file, --fuzzy, --multi, --expect-checksum, --line-ending, --dry-run, --preserve-timestamps, --partial, --fuzzy-threshold, --replace-all, --wal-policy, --strict-atomic, --backup, --no-backup, --keep-backup, --retention, --allow-sequential-drift
  • edit-loop: --strict-atomic, --allow-sequential-drift, --partial, --backup, --no-backup, --keep-backup, --retention, --syntax-check, --line-ending
  • extract: --field, -d/--delimiter, --stdin
  • extract-artifact: --max-sample-rows, --max-text-chars
  • hash: --verify, --stdin, -r/--recursive, --exclude
  • list: -d/--depth, -l/--long, --count-by-ext, --format, --all, -g/--include, --exclude
  • locale: --set, --clear
  • move: --backup, --no-backup, --keep-backup, --retention, -f/--force, --dry-run, --preserve-hardlinks
  • outline: --language, --kind, --positions, --format
  • prune-backups: --max-age-secs, --max-count, -r/--recursive, --dry-run
  • query: --language, -Q/--query, --tree, --kinds, --positions
  • read: --lines, --all, --number, --no-comments, --symbol, --enclosing, --line, -C/--context, --head, --tail, --stat, --format, --bytes, --verify-checksum, --grep, --crop, --max-tokens, --force, --pages, --page, --dpi, --ocr, --high-res
  • regex: --stdin, -d/--digits, -w/--words, -s/--spaces, -r/--repetitions, -i/--case-insensitive, --no-anchors
  • replace: --strict-atomic, --regex, -w/--word, -F/--literal, --backup, --no-backup, --keep-backup, --retention, -g/--include, --exclude, --preview, -n/--max-replacements, --expect-checksum, --dry-run, --preserve-case, --fuzzy, --fuzzy-threshold, --progress-every, --preserve-timestamps
  • rollback: --strict-atomic, --timestamp, --latest, --verify, --backup, --no-backup, --keep-backup, --retention, --dry-run
  • scope: -l/--language, --query, --pattern, --delete, --action, --replace-with, -g/--include, --exclude, --dry-run, --backup, --no-backup, --keep-backup, --retention, --strict-atomic
  • search: -e/--regex, -F/--fixed, -w/--word, -i/--case-insensitive, -S/--smart-case, -C/--context, -g/--include, --exclude, -c/--count, -l/--files, -m/--max-count, -U/--multiline, --invert, --sort, --include-fifo, --max-filesize, --max-columns, --binary, --no-begin-end, -P/--pcre2, --target, --offset, --limit, --format
  • semantic-merge: --strict-atomic, --base, --ours, --theirs, --output, --fail-on-conflict, --write-conflict-markers, --no-conflict-markers, --expect-checksum, --dry-run, --backup, --no-backup, --keep-backup, --retention
  • semantic-search: --k, --min-score, --index-dir, --include-backups
  • set: --strict-atomic, --dry-run, --backup, --no-backup, --keep-backup, --retention, --preserve-timestamps
  • stat: --lines, --all, --number, --no-comments, --symbol, --enclosing, --line, -C/--context, --head, --tail, --stat, --format, --bytes, --verify-checksum, --grep, --crop, --max-tokens, --force, --pages, --page, --dpi, --ocr, --high-res
  • transform: --strict-atomic, -p/--pattern, -r/--rewrite, -l/--language, -g/--include, --exclude, --dry-run, --rules, --inline-rules, --backup, --no-backup, --keep-backup, --retention, --verify-parse
  • wal-heal: --threshold-secs, --max-duration-ms, --dry-run
  • wal-stats: --dry-run
  • watch: --debounce-ms, --max-events, --idle-exit-ms, --checksum, --gitignore
  • write: --backup, --no-backup, --keep-backup, --retention, --max-size, --append, --prepend, --syntax-check, --durability, --expect-checksum, --allow-shrink, --line-ending, --allow-empty-stdin, --no-checksum-when-empty, --wal-policy, --strict-atomic, --preserve-timestamps, --require-backup, --confirm, --require-large-ack, --ack-overwrite, --auto-rotate, --risk-threshold, --dry-run
  • config, get, recipe, scratch, sparse and verify take no local flag, and their options come from the global set and from their own subcommands

Configuration

  • Precedence: CLI flags, then the workspace .atomwrite.toml, then the global XDG config.toml (the two files are merged key by key), then the built-in defaults
  • config path|list|get|set|unset|doctor is the only way to change a persistent tuning key
  • config set KEY VALUE --scope global|workspace and config unset KEY --scope global|workspace require --scope (exit 2 without it, nothing is touched), and config path takes it too
  • config list and config get KEY take no --scope: they show value, origin (default, global, workspace or explicit with --config PATH) and default
  • Scope global is the XDG config.toml (mode 0600), scope workspace is .atomwrite.toml in the workspace root
  • set keeps comments, validates the whole file and writes by tempfile-fsync-rename without a backup; the terminal event carries target_resolved and target_source: "argv"
  • An unknown key or a value out of range exits 65 and writes nothing; a file that is not valid TOML is never rewritten (set and unset exit 78); config works even with a broken config.toml
  • A flag still wins over the configuration (for example extract-artifact --max-text-chars)
Key Type Default Accepted Scopes
fuzzy.mode string auto auto, aggressive, off global, workspace
fuzzy.threshold float 0.7 0.0 to 1.0 global, workspace
fuzzy.threshold_aggressive float 0.5 0.0 to 1.0 global, workspace
fuzzy.threshold_context float 0.8 0.0 to 1.0 global, workspace
fuzzy.threshold_jw float 0.85 0.0 to 1.0 global, workspace
fuzzy.soft_floor_delta float 0.05 0.0 to 1.0 global, workspace
fuzzy.max_content_bytes integer 268435456 (256 MiB) 1 or more global, workspace
write.confirm_large_bytes integer 102400 (100 KiB) 1 or more global, workspace
log.file boolean false true, false global, workspace
log.rotation string daily daily, hourly global, workspace
storage.home string none a directory global only
locale string none (resolved) en, pt-BR global only
concurrency.max_threads integer 16 1 to 256 global, workspace
concurrency.ram_fraction_percent integer 50 1 to 100 global, workspace
search.max_inflight_bytes integer 8388608 (8 MiB) 4194304 (4 MiB) to 1073741824 (1 GiB) global, workspace
summary.errors_max_entries integer 50 1 to 1000 global, workspace
lock.acquire_timeout_ms integer 5000 100 to 3600000 global, workspace
retry.base_ms integer 50 1 to 60000, not above retry.max_ms global, workspace
retry.max_ms integer 400 1 to 60000 global, workspace
retry.max_elapsed_ms integer 2000 1 to 3600000 global, workspace
extract.max_sample_rows integer 10 0 to 100000 global, workspace
extract.max_text_chars integer 400 1 to 10000000 global, workspace
extract.max_sheets integer 32 1 to 10000 global, workspace
extract.max_slides integer 64 1 to 10000 global, workspace
read.max_output_bytes integer 24000 1 or more global, workspace
read.number boolean false true, false global, workspace
search.format string ndjson ndjson, text global, workspace
output.profile string full full, lean global, workspace
apply.max_ellipsis_lines integer 200 1 or more global, workspace
delete.backup boolean absent true, false global, workspace
  • read.max_output_bytes is the ceiling read honors before it prints the next --lines on stderr and marks the NDJSON event with truncated: true plus the next hint, and --all is the per-call way past it
  • read.number makes --number the default, and search.format makes --format text the default, while an explicit flag still wins
  • output.profile = "lean" drops the digests, the byte counts and the timing from a successful mutation record, and full is the default
  • apply.max_ellipsis_lines bounds the span a ... line may match inside a SEARCH block
  • delete.backup is absent by default and inherits defaults.backup, because a rewrite copies and a delete does not unless the call asks
  • A scope outside the last column exits 65 (for example config set storage.home /srv --scope workspace)
  • These fields are config keys like every row above, and each answers config get: defaults.backup, defaults.retention, defaults.line_ending, defaults.max_filesize, write.shrink_block_percent, write.auto_rotate_max_age_secs, write.content_risk_patterns, watch.idle_exit_ms, watch.debounce_ms, search.context, search.smart_case, search.max_filesize and search.max_columns; an unknown section in .atomwrite.toml makes read exit 78 (measured)
  • Files on Linux: global config $XDG_CONFIG_HOME/atomwrite/config.toml, saved locale $XDG_CONFIG_HOME/atomwrite/locale, log $XDG_STATE_HOME/atomwrite/logs/atomwrite.<date>.jsonl (only with log.file = true, file mode 0600, directory 0700), parsers $XDG_CACHE_HOME/tree-sitter-language-pack/v1.22.1/
  • macOS and Windows resolve the same files through the directories crate; those paths were read from the code and not run there
  • storage.home moves config and state under {home}/config and {home}/state
atomwrite --workspace . config get fuzzy.max_content_bytes
atomwrite --workspace . config set fuzzy.max_content_bytes 1000000 --scope global

Environment Variables

  • Product policy: atomwrite does not use product environment variables for runtime knobs (workspace, backup, locale, log filter, home override, WAL, etc.)
  • Configure via CLI flags and XDG storage / .atomwrite.toml (and optional --config)
  • doctor reads no environment variable: host detection (WSL, container, Kubernetes, Termux, Flatpak, Snap) uses file probes only, and it is not a configuration API
  • Dependencies read some variables on their own, never as atomwrite configuration: the OS-language crate (LANG, LC_ALL, LC_MESSAGES, LANGUAGE), the XDG crates (HOME, XDG_*) and the parser download client (ALL_PROXY, HTTPS_PROXY, HTTP_PROXY, NO_PROXY); see ADR-0057 in docs/decisions/
  • Shell-level variables used by your agent harness (PATH, etc.) are outside atomwrite product config

Network And Privacy

  • No telemetry: atomwrite sends nothing about you or your files
  • The only network traffic is the parser download used by query, outline and write --syntax-check (the last one read from the source, not run); transform, scope and codemod use ast-grep parsers compiled into the binary
  • On the first use of a language, the binary downloads the parser bundle for the platform from https://github.com/xberg-io/tree-sitter-language-pack/releases/download/v1.22.1/ (for Linux x86_64 the file parsers-linux-x86_64.tar.zst, 23031807 bytes in the manifest, about 22 MiB, plus 3.9 MiB of extracted parser libraries for the six languages this machine used so far); the manifest carries a sha256 for each platform bundle
  • The cache lives at $XDG_CACHE_HOME/tree-sitter-language-pack/v1.22.1/ (bundles, libs, manifest.json), and the download happens only while the needed parser is missing there
  • The download client honours the proxy variables listed in Environment Variables
  • Offline use: fill the cache directory on a host with network access and copy it to the offline host
  • A panic writes a crash report file report-<uuid>.toml in the OS temporary directory (through the human-panic crate) and prints its path; nothing is transmitted, and you choose whether to send it
  • Details: PRIVACY.md

Exit Codes

  • Error envelopes on stdout are always NDJSON with ok: false (and usually match_count); read exit / code before treating a line as success
  • The list below is derived from the error enum and checked against agent-surface and --help
  • 0 SUCCESS: success
  • 1 NO_MATCHES: no matches found (search, replace, transform, scope; not a crash)
  • 1 DOCTOR_CHECKS_FAILED: doctor --strict found failed or warned checks (the report event carries every check)
  • 2 ARGUMENT_PARSE_ERROR: command line rejected by the parser
  • 2 UNSUPPORTED_COMBINATION: two flags this verb refuses to combine (read flag and other in the envelope)
  • 4 FILE_NOT_FOUND: file not found
  • 4 GLOB_NO_MATCHES: pattern expanded to no file
  • 13 PERMISSION_DENIED: permission denied
  • 28 DISK_FULL: disk full (no space left on device)
  • 30 QUOTA_EXCEEDED: quota exceeded
  • 65 INVALID_INPUT: invalid input (bad arguments or malformed data, including empty stdin without --allow-empty-stdin and a file above [fuzzy].max_content_bytes under fuzzy)
  • 65 MATCH_FAILED: pattern did not match, or an edit-loop pair has no match (read best_candidate)
  • 65 MATCH_AMBIGUOUS: pattern matched more than once (read best_candidate)
  • 65 BINARY_FILE: binary content rejected
  • 65 FILE_TOO_LARGE: file above the size limit
  • 69 GRAMMAR_UNAVAILABLE: the tree-sitter grammar for a language is not on this host (class precondition_failed, retryable false)
  • 73 CROSS_DEVICE: cross-device rename (filesystem boundary)
  • 74 IO_ERROR: I/O error
  • 78 CONFIG_INVALID: configuration invalid, or a verb that needs a feature missing from this build (slim)
  • 79 SYMLINK_BLOCKED: symlink refused by the jail; pass --follow-symlinks to resolve it and check the target against the workspace
  • 80 TRANSACTION_ROLLBACK_FAILED: batch transaction rollback failed (backups kept)
  • 81 CHECKSUM_VERIFY_FAILED: checksum verify failed
  • 82 STATE_DRIFT: state drift (checksum mismatch, optimistic lock failed)
  • 83 LOCK_TIMEOUT: lock not acquired within lock.acquire_timeout_ms (default 5000)
  • 84 MERGE_CONFLICT: semantic-merge found conflicts
  • 85 FIFO_DETECTED: FIFO detected (named pipe cannot be atomically written)
  • 86 DEVICE_FILE: device file detected (block or character device)
  • 87 EDIT_LOOP_PARTIAL: edit-loop --partial wrote the matching pairs and some pairs missed
  • 88 SYNTAX_ERROR_DETECTED: syntax error detected (--syntax-check failed)
  • 89 ALL_SKIPPED_BINARY: every visited file was binary and was skipped, so nothing was searched (search; pass --binary to search it)
  • 91 EXDEV_FALLBACK_DISABLED: EXDEV fallback disabled (cross-device copy-fallback is not allowed)
  • 92 COPY_BACK_BLAKE3_FAILED: copy-back BLAKE3 verification failed
  • 93 ORPHAN_JOURNAL: orphan journal detected
  • 124 TIMEOUT: global --timeout-secs deadline exceeded (default 120; pass 0 to disable)
  • 126 WORKSPACE_JAIL: workspace jail violated (path escapes workspace)
  • 128 IMMUTABLE_FILE: file immutable (cannot modify)
  • 129 SIGHUP: terminal hangup (SIGHUP)
  • 130 CANCELLED: run cancelled before it finished (status follows the cause)
  • 130 SIGINT: interrupted by SIGINT
  • 141 BROKEN_PIPE: broken pipe (SIGPIPE or a BrokenPipe write error)
  • 143 SIGTERM: terminated by SIGTERM
  • 255 INTERNAL_ERROR: internal error

Signal Handling

  • Unix: SIGINT (Ctrl+C), SIGTERM and SIGHUP are intercepted for a cooperative shutdown with exit 130, 143 and 129; the run ends with a cancelled event that carries signal and deadline_exceeded
  • Unix: SIGPIPE has a handler, it is neither ignored nor left at its default action; the handler marks a cooperative shutdown with exit 141
  • The default SIGPIPE action killed the process before any destructor ran and left lock sidecars, journals and tempfiles of the writes in flight; with the handler every Drop runs and nothing is left behind
  • A write that fails because the reader closed exits 141 (BROKEN_PIPE) with no panic and no crash report: completions, --print-schema, --help and every verb (fixed in 0.1.37, test cli_v0137_broken_pipe_outputs)
  • Very short output that fits in the pipe buffer (for example --version | head -1) can finish before the reader closes and then exits 0
  • A write that hits the closed pipe fails with BrokenPipe and exits 141 as well, and a parent that ignores SIGPIPE gets the same exit 141
  • A repeated SIGPIPE never forces an exit; a repeated SIGINT, SIGTERM or SIGHUP does
  • Windows: Ctrl+C intercepted via console handler; there is no SIGPIPE, and BrokenPipe from std follows the same path (not validated there)
  • First signal: sets the shutdown flag and prints a message on stderr in the active locale (atomwrite: shutting down... with --locale en)
  • Second signal: immediate process termination via _exit (Unix) or exit (Windows)
  • Walker threads (search, replace, transform, scope) stop between files
  • Batch operations stop between operations
  • Stdin verbs (write, edit and the others) obey the signals and the deadline while stdin is still open: SIGTERM exits 143 (measured)

Error Handling

  • All errors emit a JSON object on stdout with ok: false plus code, exit, message and suggestion
  • Error fields: code, exit, path when there is one, suggestion and workspace
  • -v restores the diagnostic prose the default envelope drops: message, error_class, retryable and suggestion_lang
  • Every summary that publishes files_failed adds partial: true and errors[] beside it (each entry has path, code and message, path is <walk> when the walk failed without naming a file, at most 50), and that covers search, transform, replace, scope, count, list, sparse, semantic-search and case
  • Error classes: permanent, transient, conflict, precondition_failed
  • 38 error variants in src/error/variants.inc.rs, counted with rg -c '#\[error' src/error/variants.inc.rs
  • Transient and conflict errors set retryable: true
  • The suggestion field provides actionable recovery guidance for agents in the active locale, and suggestion_lang names that locale under -v
  • See docs/schemas/error-output.schema.json for the full contract

Performance

  • Single binary; on Linux it links dynamically against the system C library and nothing else (ldd on the v0.1.37 build lists libgcc_s.so.1, libm.so.6 and libc.so.6)
  • Release builds use LTO, single codegen unit, and symbol stripping
  • Memory-mapped file reads via memmap2 for large files
  • Parallel multi-file work is the default modus operandi: ignore::WalkParallel / collect_files_parallel (search content + --target files, replace, count, transform, scope, recursive discovery) + rayon (hash, backup, multi-path + recursive delete/copy, case multi-path, semantic-search scoring, non-txn batch, prune multi-target, sparse read, diff dual-read)
  • Worker bound: --threads / -j / --max-concurrency (default = the smaller of the logical CPUs, 16 threads and the RAM cap; an explicit -j N is honoured as asked; see src/concurrency.rs)
  • Small-file latency: medians of 5.6 ms (read) and 6.7 ms (write) in one session of 20 runs, and 9.4 ms and 9.7 ms in another of 30 runs, whole process included; machine: Intel Xeon E5-2699 v3 2.30 GHz with 72 logical CPUs, Linux, working directory on tmpfs, measured on 2026-10-19 while other jobs ran on the same host

Known Limits

  • replace --expect-checksum compares the checksum under the lock, so a writer that lands between the check and the write is detected as exit 82 STATE_DRIFT; verbs that read, change and write back (edit, edit-loop, replace, set, del, config set, apply, write --append) serialize under the lock
  • A target name long enough to push the sidecar past 255 bytes gets a short BLAKE3 digest as the sidecar name, so a 240-byte target locks instead of failing with ENAMETOOLONG; a 244-byte target was proven with replace
  • The lock is taken on the target's own inode, so a file the caller owns inside a read-only directory is written in place instead of refused with exit 13
  • The .<target>.atomwrite.lock sidecar is best effort and its failure no longer aborts the write, because an older release that holds only that file still meets the inode lock
  • The RAM ceiling for the automatic worker count reads MemAvailable from /proc/meminfo on Linux only and ignores container (cgroup) limits
  • Without --sort-by, the order of search results varies from run to run because the walkers run in parallel
  • case expands a directory argument through the shared walker and honours --include and --exclude; a directory that yields no file exits 65 instead of succeeding with nothing renamed
  • query, outline and write --syntax-check detect only 23 languages by extension; --language NAME works for a parser already extracted in the local cache, and exits 65 unsupported language override for a language not yet used
  • extract-artifact emits a single artifact event and no summary, so a successful run has no event with ok: true
  • locale --set and locale --clear print the resolved language from before the change; a following atomwrite locale shows the new state
  • macOS and Windows were not run; the Unix-only code has cfg(windows) pairs that were only compiled with cargo check from Linux

Library API

  • The supported interface of atomwrite is the CLI and its NDJSON contract; the crate also ships a Rust library target that is not covered by that promise
  • The public Rust API of v0.1.37 changed against the published 0.1.36 in ways SemVer treats as breaking: cargo semver-checks 0.50.0 reported 10 failed major checks (removed locale::Idioma and locale::{negotiate_to_idioma,resolved_idioma}, removed LocaleSource::EnvVar, removed signal::reset_sigpipe, four removed lock constants, LockGuard no longer Send and Sync, new Commands variants and new public struct fields without #[non_exhaustive], and watch no longer a default feature)
  • Pin atomwrite as a library dependency to an exact version (=0.1.38) and read CHANGELOG.md before every upgrade

Troubleshooting FAQ

atomwrite write hangs with no output

  • Ensure you are piping content to stdin
  • write reads from stdin and waits for EOF
  • Example: echo "content" | atomwrite --workspace . write file.txt

search returns exit code 1

  • Exit code 1 means zero matches were found
  • This is expected behavior, not an error
  • Check the pattern and target path

search with a pattern that starts with a hyphen

  • A pattern that starts with - goes after --, the way rg and grep do it
  • atomwrite --workspace . search -F -- '-deprecated' src/ searches for that literal text
  • Without -- the token is a flag the verb does not have, so the run exits 2 and prints the parser tip to pass '-Z' as a value, use '-- -Z'

search, replace and transform exit 4 although they printed matches

  • A root named in the invocation that does not exist fails the run even when the other roots matched, in search, replace and transform
  • The matches still reach stdout, and the summary carries ok: false, code: FILE_NOT_FOUND, files_failed and the path in errors[]
  • A file that fails during the walk is the other case: partial: true joins the summary, and the run keeps exit 0 in search, replace, transform, scope, case, sparse, semantic-search and backup, while count, list, hash, copy, delete and prune-backups exit with the first failure because their answer is a total over the tree; the contract is ADR-0077 (docs/decisions/0077-answer-family-and-walk-honesty.md)

cross-device rename fails with exit 73

  • The source and destination are on different filesystems
  • atomwrite falls back to copy+delete for move across devices
  • Use copy followed by delete as an alternative

checksum mismatch with exit 82

  • Another process modified the file between read and write
  • Re-read the file to get the current checksum
  • Retry the operation with the updated --expect-checksum

workspace jail violated with exit 126

  • The target path resolves outside the --workspace boundary
  • Verify the path does not contain .. traversals or symlinks escaping the workspace

syntax check failed with exit 88

  • The real tree-sitter syntax check found a syntax error in the file
  • Inspect the first error line/column/kind/message in the JSON error envelope
  • Fix the syntax and retry, or remove --syntax-check to bypass

empty stdin rejected with exit 65

  • The guard rejected 0 bytes from stdin as a likely upstream-pipeline failure
  • Pass --allow-empty-stdin to confirm the empty input is intentional
  • Or pipe real content: echo "x" | atomwrite --workspace . write file

edit hangs (or fails with exit 65) on --after-line/--after-match/--between/--multi

  • These modes read replacement content from stdin, same as write
  • A terminal stdin fails fast with exit 65 INVALID_INPUT instead of blocking forever
  • Pipe content: echo "new text" | atomwrite --workspace . edit file.rs --after-line 10
  • Or use --new-file <PATH> where the mode accepts it
  • --after-match/--before-match/--between/--after-line/--before-line/--range/--delete-range reject --old/--new/--old-file/--new-file at parse time (exit 2): these modes only take content from stdin

watch exits 78

  • The default binary is built without the watch feature
  • Rebuild with cargo install --path . --locked --force --features watch

MSRV And Targets

  • Minimum supported Rust version: 1.90 (edition 2024); rust-toolchain.toml pins channel 1.94 for development
  • Locales: en and pt-BR
  • Linux x86_64 is exercised end to end
  • Windows is checked with cargo check --target x86_64-pc-windows-gnu from a Linux host only
  • macOS was not exercised on a real Mac

Documentation

Architecture

  • See ARCHITECTURE.md for module map, data flow, and design decisions
  • See docs/decisions/ for the index of 77 ADRs (0019 to 0095; 3 more live in docs/adr/)
  • Verification is running the program, not a test suite: every claim in this README is backed by a real invocation of the binary, and docs/TESTING.md describes the harness for those cases a maintainer chooses to keep
  • See docs/schemas/ for the JSON Schema contracts of the NDJSON output, where 54 files describe the 55 names atomwrite --print-schema lists

Contributing

Security

  • See SECURITY.md for vulnerability reporting
  • See SECURITY.md section "Known Security Advisories" for resolved and active advisories
  • See PRIVACY.md for data handling

Changelog

License