You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
One CLI replaces dozens of file-manipulation tool calls your agent makes today
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
Reading and Running Time
Reading time is about 51 minutes for the whole guide, DERIVADO 11822 words outside the code fences at 230 words per minute
First Command in 60 Seconds takes 2 minutes to read and run
Every example ran on Linux against atomwrite 0.1.38 inside an isolated home directory
A plain write finished in 8 ms on the measurement host, so examples answer at once
The first AST call downloads tree-sitter parsers, which takes seconds (see Prerequisites)
macOS and Windows were not measured for this guide
Examples use a POSIX shell, and on PowerShell you pipe text with 'text' | atomwrite ...
When you are done, the COOKBOOK holds longer recipes
What's New in v0.1.38 (2026-11-04)
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 and no caller branched on it alone; a consumer that read it switches to .ok == false
read --format raw stops at 24,000 bytes and prints one stderr line with the --lines that reads the rest, and --all removes the cap
read --format raw on a single target honors --max-filesize and exits 65 FILE_TOO_LARGE instead of streaming a file above the ceiling
--symbol NAME, --line N --enclosing, repeatable --lines and --no-comments choose the window by syntax tree
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
A file whose image signature is damaged fails read with exit 65 INVALID_INPUT instead of returning its bytes as text
A picture arrives as one data: URI, --high-res raises the tier to 2 576 pixels and 4 784 visual tokens, and a picture already sent in this session answers with a short token line unless --force is given
read doc.pdf returns the text of every page, --pages selects windows, needs_vision names a page that carries no text, and --ocr recognizes characters on a picture or a rendered page in pure Rust
search --format text writes one path:line:col:text per match, and a tree where every file was binary exits 89 ALL_SKIPPED_BINARY instead of NO_MATCHES
apply matches whole lines, applies several files in one invocation, publishes the changed lines with --show, and sorts the targets before it takes the locks
The lock targets the inode with std::fs::File::try_lock, and delete takes the same lock before it hashes and unlinks, so a deletion and a write to one target no longer interleave
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
[defaults].backup = true reaches delete and batch delete, and [delete].backup wins over it for delete only
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)
What's New in v0.1.37 (2026-10-31)
Pin agents to ^0.1.37 with cargo install atomwrite --locked --version "^0.1.37"
The surface has 47 subcommands (SSOT), and the new one is config
config reads and writes 37 persistent keys with path|list|get|set|unset|doctor
--scope global|workspace is required on config path, config set and config unset
Global stream flags --sort-by, --dedupe, --aggregate and --diff-against shape the data channel
--join KEY:SUFFIX reads an NDJSON table from stdin once and attaches the matching rows to each data event
Global flags --redact, --normalize-encoding, --max-output-bytes and --allow-truncate guard what leaves the process
--max-output-bytes and --allow-truncate are rejected with exit 2 unless both are present
--print-schema [NAME] lists the 55 schema names or emits one schema
--non-interactive is accepted on every command, while --confirm-side-effect is accepted on the verbs that change bytes on disk and rejected with exit 65 on the rest
extract-artifact reads PDF, OOXML and image headers and never emits raw artifact bytes
Every success envelope carries ok: true, and a failing run ends in one terminal event with ok: false
Every summary that publishes files_failed publishes partial beside it, and that covers search, replace, transform, scope, count, list, sparse, semantic-search and case
search, replace, transform, scope, case, sparse, semantic-search and backup keep exit 0 when they answered and some file failed, 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)
edit-loop is all-or-nothing, and edit-loop --partial exits 87 (EDIT_LOOP_PARTIAL)
A fuzzy match above [fuzzy].max_content_bytes (256 MiB) fails with exit 65, and --fuzzy off stays exact-only
Exit codes to branch on are 65, 78, 83, 84, 87, 124, 129, 141 and 143
The write lock is automatic on Unix and on Windows, and a timeout exits 83 (LOCK_TIMEOUT)
watch left the default build, and the default binary answers it with exit 78
Changes That Can Break a Consumer
A summary that fails carries ok, code and exit itself, and no second error line follows it
Read partial and files_failed in every summary that publishes them before trusting a result, and read errors[] as the first summary.errors_max_entries causes (50 by default)
Cancelled scans end on a cancelled envelope with deadline_exceeded, so the last line may be a cancel record
--print-schema must come before the command name, and an unknown name exits 65 with an error envelope
replace, scope, codemod, case, delete, backup and prune-backups need explicit paths, or exit 2
wal-heal and scratch init take a positional path, and recipe run needs --path or --target
transform needs its positional paths, and a call without them is a usage error
edit-loop with an unmatched pair exits 65 and writes nothing, where it used to write the matching pairs
A missing pair reports MATCH_FAILED with best_candidate, where it used to report INVALID_INPUT
doctor no longer reports its former CI and privilege fields, and agent-surface no longer has the tool-protocol key
An unknown key or invalid file in config.toml or .atomwrite.toml exits 78
The workspace .atomwrite.toml and the global config.toml are merged key by key
watch needs a build with --features watch, or the call exits 78
The default build has the features core, ast, lang-rust, lang-ts and lang-py
watch and lang-go are opt-in, and --features full turns on everything
The default binary answers watch with exit 78 and the hint cargo install atomwrite --features watch
Tree-sitter parsers are not compiled in, and they are downloaded the first time a language is used
outline, query, scope, transform, codemod, sparse outline and write --syntax-check trigger the download
The download is a 23,031,807 byte bundle for linux-x86_64 (DERIVADO: 23031807 / 1048576 = 21,97 MiB), and it unpacks into 3,991,360 bytes across 6 shared libraries (DERIVADO: 3991360 / 1048576 = 3,81 MiB), measured on a cold cache on 2026-10-31
Parsers land in $XDG_CACHE_HOME/tree-sitter-language-pack/v1.22.1/, and ~/.cache/... is the Linux default
The bundle comes from the xberg-io/tree-sitter-language-pack release v1.22.1, whose manifest lists 372 languages, and the HTTP client follows the system proxy
For a host without network, fill the cache directory on a connected host first (see ADR-0057)
atomwrite doctor reports the check grammar_cache with how many of the 23 grammars the AST verbs can need are already on disk, so the operator learns the condition before the run and not in the middle of it
A grammar that is missing and cannot be downloaded is exit 69 GRAMMAR_UNAVAILABLE with class precondition_failed, never exit 255, because the host is offline and the tool is not broken
human-panic writes a panic report as a local file in the temporary directory and sends nothing anywhere
See INSTALL.md for Windows build tools and troubleshooting
First Command in 60 Seconds
Pick a working directory and store its absolute path once
--workspace anchors the jail, so a wrong value moves the jail in silence
Expect typeread with modestat or full, and lines_total when you read a slice
--format ndjson is the default, and --format raw writes the bytes to stdout without an envelope
--bytes is kept for old pipelines and means the same as --format raw
The default answer stops at read.max_output_bytes on a complete line, aligned to the end of the last outline item, and publishes truncated plus a ready next range
--format raw prints the next range on stderr and separates non-adjacent ranges with --, so a reader can tell where one range ends
Several operands concatenate in argv order, and an operand that names an existing path is read as it is
An operand with *, ? or [ that does not exist is expanded by the binary against one level of its directory, in byte order, skipping hidden names
A missing file, a directory or a glob that matched nothing prints one line on stderr and the other targets still reach stdout, with the first failure's status
--lines, --line, --context, --head, --tail, --grep, --stat and --verify-checksum take a single target and exit 65 when several are named
** is not supported, and only the final name may be a pattern: a wildcard in a directory name exits 65 instead of expanding halfway
--verify-checksum HASH exits 81 (CHECKSUM_VERIFY_FAILED) with an error envelope on a mismatch
--grep REGEX keeps only matching lines, and an invalid regex exits 65
--no-comments needs --number, because dropping lines without keeping the file's own numbers would point the next apply at the wrong one, and the call exits 65 without it
A picture is answered as one data: URI line on stdout, because that is the shape the vision channel accepts
The type of a picture comes from the magic bytes, and the size follows the 28-pixel patch rule
--max-tokens N shrinks the picture until the rounded patch count fits N, and --crop X,Y,W,H cuts the region before the resize
--high-res moves the ceiling to 2576 pixels and 4784 visual tokens, and the standard tier stays at 1568 of each
The EXIF orientation is applied, and a picture above 100 megapixels is refused before the decode
A file whose magic bytes name a picture but whose header is broken is refused with exit 65 INVALID_INPUT, and the name stays in target_resolved
HEIC and HEIF pictures are decoded like any other picture, and a still image is converted after the decode, so the vision channel receives a format it reads
--stat on a picture reports width, height, format, visual tokens and GIF frame count without sending pixels
A picture already delivered in this session is answered as the token line [image delivered earlier in this session: <hash>], because a second copy of the same pixels costs the whole picture again
--force sends the picture again inside the same session, and it is the only way to repeat a delivery that the session cache would skip
read doc.pdf returns the text of every page, and --pages N or --pages A:B selects windows, repeatable in one call
A page that carries no text is named in needs_vision, because a scanned page is not an empty document
--page N renders one page and answers with the same data: URI a picture uses, so a scan never reaches the model as a header alone
--dpi N sets the render resolution, and --stat reports the size of the rendering without sending it
--ocr recognizes the characters on a picture or a rendered page and answers with the text in content, exactly where a text file puts it
The recognition models are fetched once into the cache directory, so the first --ocr run needs a network and every later run does not
A rendered page goes to recognition at 300 dpi by default, because small print at 150 dpi is too coarse to separate letters
--crop sharpens recognition on a wide picture, because the engine reads a line better when the line is a larger share of the frame
--ocr on a file that carries text exits 65 instead of pretending, because the flag reads pixels and that file has characters
edit
Edits a file by exact text, by line number or by marker, and the write is atomic
A success line carries checksum_before, checksum_after, lines_before, lines_after and mtime_preserved
Expect a match event with path, line_number, lines, byte_offset and submatches
Exit 1 with NO_MATCHES means zero matches, and it is not a crash
--regex (-e), --fixed (-F), --word (-w), --case-insensitive (-i) and --smart-case (-S) pick the pattern mode
--context N (-C), --max-count N (-m), --invert, --multiline (-U) and --pcre2 (-P) refine matching
A pattern that starts with - goes after --, like rg: search -F -- '-deprecated' src/; without it the token is an unknown flag and the run exits 2
--count (-c) and --files (-l) change the event shape, and --target content|files|both picks what is searched
--include (-g) and --exclude filter by glob, and --sort path|modified|created|none orders files
--max-filesize BYTES skips larger files (default 10 MiB, counted in files_skipped), and --max-columns N cuts long lines
--binary also searches binary files, and --include-fifo opens named pipes, which the default skips
--no-begin-end suppresses the begin and end events of a file with no match, which keeps an empty search clean
--offset N and --limit N page through match events
Read partial and files_failed on the summary before you trust a result, and a root named in the invocation that does not exist fails the run with exit 4 even when it matched, in every summary that publishes them
replace
Replaces text across files with atomic writes, and the path argument is mandatory
Fixed strings use the fuzzy cascade after an exact miss, and --regex keeps the regex engine
Expect typeplan with would_modify, then typedeleted with checksum_before, then a summary
A directory without --recursive (-r) exits 65 and names the missing flag
delete -r <dir> without a narrowing filter owns the whole named tree and removes hidden, ignored, linked, fifo, socket and device entries, and a link is never followed
A run narrowed by --include, --exclude, --older-than or --limit exits 0 with dirs_left and entries_left, and an unfiltered run that leaves a directory exits with the measured I/O code and the surviving path in errors[]
--hidden, --no-gitignore and --follow-symlinks are no-ops on delete and only emit one diagnostic line
--dry-run works like --plan, and --older-than 1d filters by age with the suffixes s, m, h, d and w
--include (-g) and --exclude filter by glob, and --no-backup is accepted and turns off the copy that [defaults].backup = true would request, because a delete copies only when the call asks
delete --confirm and delete --yes (-y) are rejected with exit 65, so run again without them
--backup with a directory root is refused before any change, because the copy would be created inside the tree, so run copy -r first or pass --no-backup
A delete copy is never auto-removed, so clear it with prune-backups; --keep-backup only adds a warning and --backup is what creates the copy
count
Counts lines, blank lines and bytes, by extension or by size
Expect typetransformed with matches and replacements, then a summary
Exit 1 means zero matches, and the paths are required
--rules FILE and --inline-rules YAML apply a list of rules, in the format shown under codemod, and the single summary adds the failures of every rule in files_failed, partial and errors[] beside the counters
A file that fails counts once in files_failed and once in errors[], whatever the number of rules, because each rule walks the whole tree
files_visited, files_matched and total_replacements still sum per rule, because every rule is a full pass
--verify-parse checks that the result still parses, and --include and --exclude filter files
batch
Runs operations from an NDJSON manifest, with write, replace, delete, edit, hash, move and copy
--transaction makes it all-or-nothing, and a failure rolls back
Expect one batch_op per line and a closing summary with succeeded, failed and committed
--file PATH reads the manifest from a file, and the manifest is read one line at a time
move and copy operations take "force":true to overwrite
--input-schema prints the JSON Schema of the manifest, and --batch-size N sets how many operations buffer before the summary
apply
Applies a patch from stdin: unified diff, SEARCH/REPLACE blocks, markdown-fenced or a full file
Detection is automatic, and --format auto|unified|search-replace|full|markdown forces one
A body with no marker at all is refused with exit 65, and only --format full replaces the whole file, so a malformed patch can no longer become a silent overwrite
--format full walks the same two barriers as write: --ack-overwrite above [write].confirm_large_bytes and --allow-shrink below [write].shrink_block_percent
--strict-atomic exits 91 instead of falling back to a cross-device copy, and --no-syntax-check skips the parse check that refuses a patch which breaks the file
A ---/+++ header with no @@ hunk is refused with exit 65, because applying it as a full file would overwrite the target with the patch text
Expect typeplan on the dry run and typeapplied with format_detected, hunks_applied and backup_path when the run made a copy
A unified diff must match context and removed lines within 64 lines, or it exits 65 with MATCH_FAILED
A SEARCH/REPLACE block that is missing fails the whole batch, and nothing is written
The target argument is optional: a patch that names its own files is applied in one call, and every file is checked before the first write
A SEARCH/REPLACE patch names a file with a bare path line just above that file's first block, and a unified patch uses its own header pair for the same job
The refusal of a file names it in target_resolved, and every applied file gets its own record with its own path and checksums
Two sections for the same file are refused, and a write that fails after another file was written restores the files already written and publishes one rollback record with the count
set
Writes a value at a dotted path in a TOML or JSON file and keeps comments and key order
The type is inferred from the value, so 9 becomes an integer and true a boolean
Expect typeconfig with action, key, value, origin and default
origin is default, global, workspace, or explicit when --config PATH is used
The 43 keys, their ranges and scopes are in the Configuration section
47 Subcommands (SSOT)
Pin agents to ^0.1.38, and read atomwrite agent-surface for the live manifest
CANONICAL_SUBCOMMAND_COUNT is 47, and the machine tree is atomwrite commands --include-globals
Command
Summary
read
Reads a file with checksum, size and lines, or only a slice
write
Creates or overwrites a file atomically from stdin
edit
Edits by exact or fuzzy text, line number or marker
delete
Deletes files or trees, owning the whole named tree on -r, copying one only when the call asks (--backup or [defaults].backup = true), or prints the plan
copy
Copies a file or tree with checksum verification
move
Moves or renames a file atomically
apply
Applies a unified diff, SEARCH/REPLACE or full file from stdin
search
Searches file contents in parallel with the ripgrep engine
replace
Replaces text across files with atomic writes and fuzzy matching
transform
Rewrites code by AST pattern with ast-grep
hash
Prints the BLAKE3 checksum of files or stdin
count
Counts lines and files by extension or by size
diff
Compares two files as unified, stat or changes
list
Lists a directory tree with metadata
extract
Picks fields from NDJSON on stdin
extract-artifact
Extracts bounded signal from PDF, OOXML and image headers
scope
Acts on comments, strings, functions and other code categories
regex
Generates a regex from example strings
calc
Evaluates math and unit conversions
completions
Prints or installs shell completions
backup
Creates timestamped backups with BLAKE3 checksums
rollback
Restores a file from a previous backup
batch
Runs NDJSON operations from stdin, optionally as one transaction
edit-loop
Applies N old/new pairs in one write, all or nothing
prune-backups
Removes old .bak files by age or count
wal-stats
Shows the state of WAL journals, read only
wal-heal
Removes stale terminal WAL journals
verify
Checks a file against an expected BLAKE3 hash
set
Writes a value at a dotted path in TOML or JSON
get
Reads a value at a dotted path in TOML or JSON
del
Removes a key from a TOML or JSON file
case
Renames identifiers between case styles in files
query
Walks the tree-sitter AST and emits nodes
outline
Emits the structure of a source file
semantic-merge
Three-way line-based merge with conflict markers
sparse
Budgeted list, read and outline for large trees
recipe
Lists and runs built-in multi-step recipes
stat
Metadata only, alias of read --stat
agent-surface
Tool manifest for agents
watch
Streams filesystem events, needs the watch build feature
codemod
Applies a YAML list of AST rewrite rules
semantic-search
Offline Jaccard ranking of files by query
doctor
Host readiness diagnostics
scratch
Creates the staging directory inside the jail
locale
Shows, sets or clears the display language
commands
Prints the full command tree as JSON
config
Reads and writes persistent tuning keys
Global Flags
Global flags go before the subcommand, and atomwrite commands --include-globals lists all 33
--json is accepted and ignored, because stdout is always NDJSON
There is no global -n, because replace owns -n
Flag
Default
Meaning
-v, --verbose
off
Raises tracing on stderr: -v info, -vv debug, -vvv trace
-q, --quiet
off
Lowers tracing: -q error, -qq off
--workspace PATH
current directory
Jail root, and every path must stay inside it
--config PATH
none
Explicit .atomwrite.toml, and it fails if missing or malformed
--color auto|always|never
auto
Controls colored output
--no-color
off
Same as --color never
--ready-file PATH
none
Test harness: writes the PID once signal handlers are ready
--no-gitignore
off
Does not respect .gitignore
--hidden
off
Includes hidden files and directories
--follow-symlinks
off
Follows symbolic links
-j, --threads N
automatic
Alias --max-concurrency, omitted or 0 means all cores up to 16, limited by RAM, and an explicit N is honored above 16
--max-filesize BYTES
1 GiB
Rejects larger files with exit 65, while search skips above its own 10 MiB limit and counts the skips in files_skipped
--timeout-secs N
120
Alias --timeout, 0 disables the deadline, and a deadline exits 124
--no-progress
off
Disables NDJSON progress events on long operations
--json-schema
off
Emits the JSON Schema of the subcommand output and exits
--print-schema [NAME] | off | Lists the 55 schema names, or emits one, and it goes before the command |
| --non-interactive | off | Declares agent-hosted execution: never prompts, never reads confirmation from stdin |
| --confirm-side-effect | off | Acknowledges a side effect where the command contract asks for it |
| --allow-truncate | off | Opt-in to value truncation, required by --max-output-bytes |
| --locale en\|pt-BR | resolved | Overrides the display language of suggestions |
| --no-auto-heal | off | Skips the startup wal-heal pass |
| --select KEYS | all keys | Alias --fields, projects events to top-level keys (CSV) |
| --filter EXPR | none | Keeps events where key=value, key==value or key!=value, repeatable AND |
| --limit N | none | Work budget: cuts the target list before the action and stops the walk at the record that reaches the cap, so at most N answer records leave; 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 | Orders buffered events by a key, path-line or line-path |
| --dedupe KEYS | off | Keeps the first event of each group of equal key values |
| --aggregate KEY[:OP[:FIELD]] | off | Collapses events per group, with OP count, sum, min or max |
| --join KEY:SUFFIX | off | Reads an NDJSON table from stdin once (up to 16 MiB) and attaches the matching rows under SUFFIX |
| --truncate-content KEY:LEN | off | Cuts string values of 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 | Replaces the stream with one stream_diff line of added, removed and unchanged counts |
| --max-output-bytes BYTES | off | Caps every string value, checksum included, and needs --allow-truncate |
--select, --filter and --limit keep error envelopes, so a failure with ok: false still appears
Ordering, dedupe, aggregate and stream diff buffer the whole run and apply at the end
--max-output-bytes cuts every string value, so never use it when the full checksum or content matters
--diff-against needs a baseline file inside the workspace, and --print-schema NAME or --print-schema=NAME both work
A tail shape flag never hides the cancelled record of an interrupted run
Expect sorted match events, then group lines such as {"kind":"file","count":11}, then cut strings, then one error envelope
The last call prints the JSON Schema of the whole search stream
--aggregate with sum reads a numeric field, as in --aggregate kind:sum:size
--diff-against compares the stream with a saved baseline, as shown next
The --join call prints {"type":"entry","path":"src/lib.rs","owners":[{"path":"src/lib.rs","owner":"alice"}]} and exits 0
--join runs once after sorting and before dedupe, select, filter and limit
An event without KEY, or with no matching row, stays as it was
Failure events and the summary are never changed by a join
A join exits 65 (INVALID_INPUT) when stdin is a terminal or the table is over 16 MiB
It also exits 65 for a non-object line, a bad KEY:SUFFIX, or a table with no string KEY
Commands that read stdin refuse with exit 65: write, edit, edit-loop, apply, extract, batch (no --file), hash --stdin, diff -
Use it with read-only commands such as search, list and count
A .gitignore or .ignore at the top of a root that cannot be read or parsed is reported in files_failed and errors[] with its own path, so a scan never hides a broken ignore file
Exit Codes and Error Classes
Read the exit code before parsing stdout, because an error envelope also goes to stdout
The table has 42 rows and 34 distinct exit numbers, and it comes from atomwrite agent-surface
atomwrite --help prints the same list under Exit codes
The error enum has 38 variants, counted with rg -c '#\[error' src/error/variants.inc.rs, and code in the envelope is a free string, not a closed enum
Exit
Code
Class
Retryable
0
SUCCESS
none
no
1
NO_MATCHES
permanent
no
1
DOCTOR_CHECKS_FAILED
precondition_failed
no
2
UNSUPPORTED_COMBINATION
permanent
no
2
ARGUMENT_PARSE_ERROR
permanent
no
4
FILE_NOT_FOUND
permanent
no
4
GLOB_NO_MATCHES
permanent
no
13
PERMISSION_DENIED
permanent
no
28
DISK_FULL
transient
yes
30
QUOTA_EXCEEDED
transient
yes
65
INVALID_INPUT
permanent
no
65
MATCH_FAILED
permanent
no
65
MATCH_AMBIGUOUS
permanent
no
65
BINARY_FILE
precondition_failed
no
65
FILE_TOO_LARGE
precondition_failed
no
69
GRAMMAR_UNAVAILABLE
precondition_failed
no
73
CROSS_DEVICE
conflict
yes
74
IO_ERROR
transient
yes
78
CONFIG_INVALID
permanent
no
79
SYMLINK_BLOCKED
precondition_failed
no
80
TRANSACTION_ROLLBACK_FAILED
permanent
no
81
CHECKSUM_VERIFY_FAILED
precondition_failed
no
82
STATE_DRIFT
conflict
yes
83
LOCK_TIMEOUT
conflict
yes
84
MERGE_CONFLICT
permanent
no
85
FIFO_DETECTED
precondition_failed
no
86
DEVICE_FILE
precondition_failed
no
87
EDIT_LOOP_PARTIAL
permanent
no
88
SYNTAX_ERROR_DETECTED
precondition_failed
no
89
ALL_SKIPPED_BINARY
permanent
no
91
EXDEV_FALLBACK_DISABLED
precondition_failed
no
92
COPY_BACK_BLAKE3_FAILED
conflict
yes
93
ORPHAN_JOURNAL
precondition_failed
no
124
TIMEOUT
precondition_failed
no
126
WORKSPACE_JAIL
precondition_failed
no
128
IMMUTABLE_FILE
precondition_failed
no
129
SIGHUP
none
no
130
CANCELLED
permanent
no
130
SIGINT
none
no
141
BROKEN_PIPE
permanent
no
143
SIGTERM
none
no
255
INTERNAL_ERROR
permanent
no
Exit 0 is success and exit 1 NO_MATCHES is a normal answer of search, replace, transform, scope and semantic-search
A failing run ends in exactly one terminal event with ok: false, and suggestion lives inside it
The default error envelope carries ok, code, exit, path when there is one, suggestion, target_resolved and target_source
Pass -v to add the diagnostic prose back: message, error_class, retryable and suggestion_lang
The same -v raises stderr logging to info, while stdout keeps its single terminal line
MATCH_FAILED adds match_count and best_candidate, and MATCH_AMBIGUOUS adds best_candidate and match_count
Classes are none, permanent, precondition_failed, transient and conflict
Retry only when retryable is true, back off on transient and re-read the file on conflict
Exit 82 means reload the checksum, exit 124 means raise --timeout-secs or narrow the path
Exit 126 is the jail and exit 143 is a clean SIGTERM
Exit 129 is SIGHUP, and exit 130 is SIGINT or a cancelled run
Exit 141 means the reader closed the pipe, so output is incomplete and the state needs a re-check
Exit 78 covers a bad config file, an unknown config key and a verb that is missing from the build
Every verb that walks a tree lists unreadable files in files_failed, partial and errors[], so read them, and errors[].path is the marker <walk> when the walk failed without naming a file of its own
A write that fails because the reader closed exits 141 (BROKEN_PIPE), including completions, --print-schema, --help and every verb
{"ok":false,"ok":false,"code":"MATCH_FAILED","exit":65,"target_resolved":["src/a.rs"],"target_source":"argv","match_count":0,"suggestion":"inspect best_candidate in the error NDJSON and adjust --old, or lower --fuzzy-threshold"}
SIGPIPE has a cooperative handler that ends the run with exit 141 after cleanup
The run never leaves a lock, journal or tempfile behind
This is a deliberate deviation from the literal rule of resetting SIGPIPE to its default action
Configuration
Precedence is flags first, then the workspace .atomwrite.toml, then the global config.toml, then built-in defaults
The two files are merged key by key, so a workspace file hides only the keys it names
--config PATH replaces both files, and config get then reports originexplicit
atomwrite has no product environment variables, so a value comes from a flag or from one of these files
Change a key with atomwrite config set KEY VALUE --scope global|workspace, and avoid editing the files by hand
A set keeps comments, validates the whole file and lands atomically, with mode 0600 for the global file on Unix
config still works when config.toml is broken, and a file that is not valid TOML is never rewritten
The terminal event of set and unset carries target_resolved, so check that the path is the file you meant
The 43 Keys
Run atomwrite config list for the live table with type, default, value, origin and scopes
log.file, log.rotation, storage.home and locale are global-only, and the workspace scope exits 65
An unknown key or a value outside its range exits 65 and writes nothing
Key
Type
Default
Accepted values
Scopes
defaults.line_ending
string
auto
auto, lf, crlf
global, workspace
defaults.backup
boolean
none (true for rewrites, false for delete)
true, false
global, workspace
delete.backup
boolean
none (falls back to defaults.backup)
true, false
global, workspace
defaults.retention
integer
5
0 to 255
global, workspace
defaults.max_filesize
integer
1073741824
1 or more
global, workspace
fuzzy.mode
string
auto
auto, aggressive, off
global, workspace
fuzzy.threshold
float
0.7
0 to 1
global, workspace
fuzzy.threshold_aggressive
float
0.5
0 to 1
global, workspace
fuzzy.threshold_context
float
0.8
0 to 1
global, workspace
fuzzy.threshold_jw
float
0.85
0 to 1
global, workspace
fuzzy.soft_floor_delta
float
0.05
0 to 1
global, workspace
fuzzy.max_content_bytes
integer
268435456
1 or more
global, workspace
apply.max_ellipsis_lines
integer
200
1 or more
global, workspace
write.confirm_large_bytes
integer
102400
1 or more
global, workspace
write.shrink_block_percent
integer
50
1 to 99
global, workspace
write.auto_rotate_max_age_secs
integer
86400
1 or more
global, workspace
write.content_risk_patterns
list of strings
the built-in risky-command list
any string
global, workspace
watch.idle_exit_ms
integer
500
0 or more (0 disables)
global, workspace
watch.debounce_ms
integer
200
1 or more
global, workspace
log.file
boolean
false
true, false
global
log.rotation
string
daily
daily, hourly
global
storage.home
string
none
a directory
global
locale
string
none
en, pt-BR
global
concurrency.max_threads
integer
16
1 to 256
global, workspace
concurrency.ram_fraction_percent
integer
50
1 to 100
global, workspace
search.context
integer
0
0 or more
global, workspace
search.smart_case
boolean
true
true, false
global, workspace
search.max_filesize
integer
10485760
1 or more
global, workspace
search.max_columns
integer
500
1 or more
global, workspace
search.max_inflight_bytes
integer
8388608
4 MiB to 1 GiB
global, workspace
search.format
string
ndjson
ndjson, text
global, workspace
read.max_output_bytes
integer
24000
1 or more
global, workspace
read.number
boolean
false
true, false
global, workspace
output.profile
string
full
full, lean
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
A flag still wins over a key, as extract-artifact --max-text-chars wins over extract.max_text_chars
fuzzy.max_content_bytes is the ceiling above which the fuzzy cascade does not run, and exact matches still work
write.confirm_large_bytes is the size above which an overwrite needs --ack-overwrite
search.format decides whether search writes NDJSON or text when --format is absent, and the flag always wins over the key
output.profile = "lean" drops the digests, the byte counts and the timing from a successful mutation record
retry.* shapes the jittered retry of lock and rename waits, and --timeout-secs still caps it
storage.home relocates config and state under {home}/config and {home}/state
atomwrite --workspace "$WS" config set fuzzy.threshold 0.9 --scope workspace
atomwrite --workspace "$WS" config get fuzzy.threshold
atomwrite --workspace "$WS" config set log.file true --scope global
The first call writes .atomwrite.toml in the workspace, and the second reports originworkspace
The third call turns on the diagnostics file, which only the global scope can do
Config File Layout
A file that is found must be readable, valid TOML and free of unknown keys
Otherwise the run exits 78 with CONFIG_INVALID
The message names the file, and a version newer than the supported value 1 also exits 78
The loader accepts version and 11 sections: [defaults], [write], [watch], [fuzzy], [search], [storage], [log], [concurrency], [lock], [retry] and [extract]
Some fields have no config key, so you set them by editing the file
Field
Meaning
defaults.backup, defaults.retention
Default backup switch and retention for mutating commands; an absent backup key keeps each verb's own default
Pass an absolute --workspace to detached processes, because they do not inherit a useful working directory
Integration With AI Agents
Every subcommand writes deterministic NDJSON on stdout, and every write returns a BLAKE3 checksum
Run atomwrite agent-surface --format json once to learn tools, exit codes and schema names
Always pass --workspace as an absolute path, and use --select, --filter and --limit to save tokens
Read the exit code before the fields, and read best_candidate after exit 65
Run --dry-run, --plan or --preview before any bulk change
Use --expect-checksum for concurrent edits, and --ack-overwrite only for intended large overwrites
Never pass delete --confirm or delete --yes, and never run watch without --max-events or a timeout
See AGENTS.md for the full agent contract and the exit code handling
Integration With Rust Crates
atomwrite is a command line tool, and the supported way to call it from Rust is a subprocess
Check the exit status first, then parse the NDJSON lines from stdout
use std::process::Command;fnmain() -> std::io::Result<()>{let ws = std::env::current_dir()?;let out = Command::new("atomwrite").arg("--workspace").arg(&ws).args(["hash","src/lib.rs"]).output()?;match out.status.code(){Some(0) => {let stdout = String::from_utf8_lossy(&out.stdout);let first = stdout.lines().next().unwrap_or("");println!("ok: {first}");}Some(code) => eprintln!("atomwrite exited with {code}: {}",String::from_utf8_lossy(&out.stdout)),None => eprintln!("atomwrite was killed by a signal"),}Ok(())}
Expect ok: followed by the hash event when run from the sample workspace
Parse each line with serde_json in a real project, and validate it against atomwrite --print-schema NAME
Common Errors
The first bullet of each entry is the symptom, and the next ones are cause and fix
Exit 65 MATCH_FAILED
An edit or edit-loop pair found no match
Read best_candidate, widen --old, or use --fuzzy auto or --fuzzy aggressive
Exit 65 on a Large or Shrinking Write
The target is above 100 KiB, or the new content is under half of the old size
Pass --ack-overwrite for the first case and --allow-shrink for the second, only when intended
Exit 82 STATE_DRIFT
The file changed since you read its checksum
Reload the checksum and decide again, or use --allow-sequential-drift in a controlled pipeline
Exit 126 WORKSPACE_JAIL
A path points outside --workspace
Use a path inside the workspace, and stage content files under .atomwrite/scratch
{"ok":false,"ok":false,"code":"WORKSPACE_JAIL","exit":126,"suggestion":"use a path inside the workspace (/home/user/project); for --old-file/--new-file stage under /home/user/project/.atomwrite/scratch/ (never OS /tmp outside the jail)","path":"/etc/passwd","workspace":"/home/user/project"}
Exit 4 FILE_NOT_FOUND
The path does not exist, and the envelope lists similar_paths
Check the spelling, or use list and search --target files to find the file
Exit 2 ARGUMENT_PARSE_ERROR
A flag does not exist or a required argument is missing, and message carries the clap usage
Run atomwrite COMMAND --help and use only flags listed there
Exit 1 NO_MATCHES
A search-like command found nothing, which is an answer and not a crash
Broaden the pattern, or review --include, --exclude and --max-filesize
Exit 78 CONFIG_INVALID
A config file is malformed, has an unknown key, or the verb is missing from the build, as with watch
Fix the file named in message, or reinstall with --features watch
Exit 88 SYNTAX_ERROR_DETECTED
--syntax-check or --verify-parse found a parse error in the result
Fix the content, and expect the first call per language to download the parser
Silent Zero From case on a Directory
case on a directory exits 0 with identifiers_total 0 and changes nothing
Pass the files instead
The prune-backups Dry Run
--dry-run takes no value, and --dry-run false reads false as a path
Omit --dry-run to apply, and name the target files whose backups you want to prune
Broken Pipe
read and search exit 141 when the reader closes the pipe, and the output is incomplete
This covers completions, --print-schema, --help and every verb, with no panic and no crash report
Modification Time And Build Systems
edit and replace update the file mtime to the end of the write, which build tools need
Cargo, make, cmake, gradle, bazel and ninja compare mtimes to decide what to rebuild
A preserved old mtime can make cargo skip a rebuild and report a stale binary as finished
Use --preserve-timestamps only for snapshots, reproducible artifacts or files outside any build
The field mtime_preserved in the response tells you which case happened
Windows Installation
Install Visual Studio Build Tools with the C++ desktop workload and Rust 1.90 or later
Use Windows Terminal or PowerShell 7 for UTF-8 output and ANSI sequences
The advisory write lock is Unix-only, and Windows has no write lock yet
See INSTALL.md for the full Windows guide and troubleshooting
Release History
This section is historical, and the details live in the CHANGELOG and MIGRATION
v0.1.36 (2026-07-31)
Historical: 46 subcommands, and scratch init staging under .atomwrite/scratch/
Failures emit ok: false and a stable match_count, so check the exit before success fields
Global --select, --filter and --limit shape stdout on every subcommand
Normalized fuzzy strategies report similarity: null, and block_anchor uses a dual gate
v0.1.35 (2026-07-19)
Large overwrites became default-deny, and --ack-overwrite is the one-shot acknowledgement
delete --plan became the way to list targets, and delete --confirm and --yes were rejected
watch always emits a final watch_summary, with a default idle exit of 500 ms
semantic-merge writes conflict markers by default
v0.1.34 and v0.1.30
The global timeout default became 120 seconds, and exit 124 marks a deadline
Fuzzy multi-apply became one-pass from left to right, with a default of one apply
Success lines gained match_count and indent_adjusted, and misses gained best_candidate
Historical: 41 subcommands at that point
v0.1.12 to v0.1.29
v0.1.12 added set, get, del, case, query and outline
v0.1.18 added wal-stats and wal-heal, v0.1.22 added edit-loop and prune-backups, v0.1.25 added verify
v0.1.23 made backups default on, and v0.1.28 unified the backup flags across the mutating commands
v0.1.29 added recipe, sparse, semantic-merge, agent-surface, watch, codemod, semantic-search and stat