atomwrite 0.1.38

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# atomwrite

> Atomic file operations for LLM agents: one CLI with 47 subcommands, NDJSON on stdout and BLAKE3 checksums on every write

atomwrite is a Rust CLI for reading, writing, editing, searching and transforming files with atomic write guarantees. Every write uses tempfile, fsync, rename and a directory fsync. Every response is NDJSON with BLAKE3 checksums. It is built for LLM agents that need reliable, structured file operations without corruption risk. This is the expanded reference: `llms.txt` is the short index of the same contract.


## 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


## Release 0.1.38
- Current version is 0.1.38 (MSRV 1.90), pin `^0.1.38`
- The default `read` stops at `read.max_output_bytes` (24,000 bytes), cuts on a complete line and at the end of the last outline item, and publishes `truncated` plus a ready `next` range; `--all` writes every byte
- `read --format raw` prints the next range on stderr and separates non-adjacent ranges with `--`, and the single-target 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 and printed
- `read --no-comments` without `--number` is refused with exit 65, because dropping lines would point the next `apply` at the wrong one
- A `--lines` range that starts after the end of the file is an `InvalidInput` error naming the total line count, never an empty success
- `search --format text` writes one `path:line:col:text` per match under a byte cap, a file skipped as binary emits a stderr notice, and a tree where every file was skipped exits `89 ALL_SKIPPED_BINARY`
- `search` accepts `-e`/`--regex` repeated and `-n` as an explicit no-op, and `search.format` makes the text shape the default while an explicit `--format` still wins
- `list --format text` writes one path per line under a cap, 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`; `sig_truncated` appears only when the signature was really cut
- `apply` takes the target from the patch when no file is named: a path line above each file's SEARCH/REPLACE blocks, or a pair of header lines in a unified patch
- Every block of every file is checked before the first write, so a block that does not match refuses the whole call and names the file and the block, and a write that fails after another file was written restores the files already written
- The SEARCH/REPLACE match is line based, a repeated SEARCH without `--all` exits `65 MATCH_AMBIGUOUS`, and a block without `=======` or `>>>>>>> REPLACE`, or with an empty SEARCH, is an error naming the block index
- `apply --format unified` accepts `@@ ... @@` and a bare `@@`, and a SEARCH block with a `...` line matches the span between its anchors under `apply.max_ellipsis_lines` (200)
- The syntax check runs before and after the swap and refuses only when the parse-error count rises, with `--no-syntax-check` as the opt-out, and the refusal publishes the `line` and `line_end` of the block
- `apply --show` returns the changed lines with their 1-based numbers, which removes the re-read that confirmed the swap
- `diff` reports a 1-based `line` plus `old_line` and `new_line`, and `--select` is honored on the terminal summary
- `delete -r <dir>` owns the whole named tree, including hidden, ignored, linked, fifo, socket and device entries, and publishes `backups_kept`, `backup_bytes` and `dirs_left` when a backup or a leftover exists
- The failure envelope carries `ok`, `code`, `exit`, `path` and `suggestion`, and `message`, `error_class`, `retryable` and `suggestion_lang` appear only under `-v`; `error: true` left the envelope
- The lock targets the inode with `std::fs::File::try_lock`, the `.<target>.atomwrite.lock` sidecar is best effort, and `delete` takes the same lock before it hashes and unlinks
- `target_resolved` is always an array in invocation order, because a path may contain the comma that used to join several targets
- `config` lists 43 keys, `output.profile = "lean"` trims a successful mutation record, and `atomwrite commands --brief` prints one line per subcommand in 5,020 bytes against 67,492 for the full tree
- `read doc.pdf --page N` renders the page as a `data:` URI, `--dpi N` sets the resolution, and `--stat` reports the size without sending pixels
- `--ocr` recognizes the characters on a picture or a rendered page and answers with the text in `content`, with the model files fetched once into the cache directory

## Release 0.1.37
- The previous release is 0.1.37 (MSRV 1.90), with 47 subcommands (the single source of truth is `atomwrite agent-surface`)
- No telemetry and no product environment variables: configuration comes from CLI flags, the workspace `.atomwrite.toml` and the global XDG `config.toml`
- New `config` verb: `config path|list|get|set|unset|doctor` over 43 keys, with `--scope global|workspace` required on `path`, `set` and `unset`
- `scratch init PATH` creates the staging directory `.atomwrite/scratch/` inside the workspace jail, so patches never go through the OS `/tmp`
- A failed run ends with exactly one terminal event carrying `ok:false`, `code`, `exit`, `error_class`, `retryable` and `suggestion`, and `match_count` is always present on match failures
- Global agent-native stdout shape: `--select`/`--fields`, `--filter` and `--limit` (long-only, there is no global `-n`), plus the stream flags `--sort-by`, `--dedupe`, `--aggregate`, `--join`, `--diff-against`, `--redact`, `--normalize-encoding`, `--truncate-content` and `--max-output-bytes`
- `--non-interactive`, `--confirm-side-effect` and `--allow-truncate` are new global flags, and `suggestion_lang` names the language of `suggestion`
- A cancelled run ends with the `cancelled` envelope, and `deadline_exceeded` separates the `--timeout-secs` deadline (exit 124) from a signal (exit 130 or 143)
- `edit-loop` is all-or-nothing: a pair with no match exits 65 with `MATCH_FAILED`, and `--partial` writes the matching pairs and exits 87
- Every summary that publishes `files_failed` publishes `partial: true` beside it, and six new summary schemas are available through `--print-schema`
- Fuzzy matching has a content ceiling, `[fuzzy].max_content_bytes` (256 MiB)
- `watch` is no longer in the default build: install with `--features watch`
- Earlier releases: 0.1.35 (large overwrite default-deny, `watch_summary`, `delete --plan`), 0.1.34 (one-shot fuzzy), 0.1.33 (runtime hang fix and one-pass fuzzy), 0.1.32 (docs.rs `doc_cfg` and multi-target), 0.1.30 (agent surface). The subcommand count grew from 41 at 0.1.29 to 47 at 0.1.37, and `CHANGELOG.md` has the exact series

## Installation

```bash
cargo install atomwrite --locked
```

- The default build has the features `core`, `ast`, `lang-rust`, `lang-ts` and `lang-py`, and `semantic-search` is always compiled
- `watch` is opt-in: `cargo install atomwrite --locked --features watch`, and the default binary answers `watch` with exit 78 and `CONFIG_INVALID`
- Slim build: `cargo build --release --no-default-features --features core`, where `outline` and `sparse outline` exit 78 with a rebuild hint
- Full build: `cargo build --release --features full`
- Parsers for 372 languages are NOT bundled: the tree-sitter parsers are downloaded on first use by `tree-sitter-language-pack` (honouring the proxy settings of the host) and cached on disk, so the install footprint stays small
- The first run of each language therefore needs the network, and `atomwrite doctor` publishes the check `grammar_cache` with how many of the 23 grammars the AST verbs can need are already cached, so the condition is known before the run
- A grammar that is missing and cannot be downloaded is exit 69 `GRAMMAR_UNAVAILABLE` with class `precondition_failed`, and never exit 255; re-run the same verb once the network is up and the pack caches the grammar
- A panic writes a local report file in the temporary directory and the CLI never transmits it

## Setup For The Examples

Set `WS` once to the absolute path of the workspace root. Every example below passes `--workspace "$WS"`, because the path jail is anchored on that value and a wrong `.` moves the whole jail silently. The examples assume a small workspace with `src/a.rs`, `src/b.rs`, `src/c.rs`, `config.toml`, `notes.txt`, `rules.yaml` and `img.png`.

```bash
WS=/absolute/path/to/workspace
HASH_A=$(atomwrite --workspace "$WS" hash src/a.rs | jaq -r .checksum)
```

## 47 Subcommands

### Core I/O
- `read` -- read files with metadata, checksum and a bounded body. `--format raw|ndjson` selects a byte stream or the NDJSON envelope, and `--head`, `--tail`, `--lines START:END` (repeatable, `--range` as an alias), `--grep`, `--number`, `--symbol NAME`, `--line N --enclosing`, `--no-comments` and `--all` narrow or widen the content. Example: `atomwrite --workspace "$WS" read --head 2 src/a.rs`
- `write` -- create or overwrite files atomically via stdin; large targets require `--ack-overwrite` (default-deny), and `--preserve-timestamps` keeps the source mtime. Example: `printf 'x\n' | atomwrite --workspace "$WS" write notes/new.txt`
- `edit` -- surgically edit by line number, text marker, or exact/fuzzy match; multi-pair `--old`/`--new` has a per-pair fuzzy cascade, `pair_results` and `--partial`; stage `--old-file`/`--new-file` under `.atomwrite/scratch/`. Example: `atomwrite --workspace "$WS" edit src/b.rs --old beta --new gamma`
- `delete` -- delete files, copying one only when the call asks (`--backup` or `[defaults].backup = true`); plan-only via `--plan`, a directory needs `-r` even for a plan, and `--confirm`/`--yes`/`-y` are rejected. Example: `atomwrite --workspace "$WS" delete --plan tmp.txt`
- `copy` -- copy files or trees with checksum verification (reflink on APFS, btrfs and XFS). Example: `atomwrite --workspace "$WS" copy src/a.rs src/a_copy.rs`
- `move` -- move or rename files atomically (EXDEV copy-fallback). Example: `atomwrite --workspace "$WS" move src/tmp.rs src/tmp2.rs`
- `apply` -- apply patches from stdin (unified diff, search/replace, full, markdown); the target is optional, so a patch that names its files lands in one call, `--show` returns the changed lines, and `--no-syntax-check` opts out of the pre/post parse. Example: `printf 'new body\n' | atomwrite --workspace "$WS" apply --format full notes.txt`

### Search and replace
- `search` -- search file contents in parallel (ripgrep engine); `--no-begin-end` disables anchor emission, `--format text` prints one `path:line:col:text` per match, and `-e`/`--regex` repeats a pattern. Example: `atomwrite --workspace "$WS" search alpha src/`
- `replace` -- replace text across files with atomic writes; `--fuzzy auto|aggressive|off` (`off` = exact-only, default `auto`) and `--fuzzy-threshold`; one-pass multi-apply on the original content (it never re-scans inserted text); default max applies is 1; if the replacement embeds the pattern it forces 1 apply; reports `strategy`, `similarity`, `match_count` and `indent_adjusted`; match errors may include `best_candidate`. Example: `atomwrite --workspace "$WS" replace --dry-run alpha omega src/`
- `transform` -- AST refactoring via ast-grep for the languages ast-grep supports (an unsupported language exits 65). Example: `atomwrite --workspace "$WS" transform --dry-run -p '$E.unwrap()' -r '$E?' -l rust src/`

### Inspection
- `hash` -- calculate BLAKE3 checksums. Example: `atomwrite --workspace "$WS" hash src/a.rs`
- `count` -- count lines and files by extension; `--by-size` lists the largest files. Example: `atomwrite --workspace "$WS" count --by-extension src/`
- `diff` -- compare two files (unified, stat or changes); `--algorithm myers|patience|lcs`, and each change carries a 1-based `line` plus `old_line` and `new_line`. Example: `atomwrite --workspace "$WS" diff src/a.rs src/b.rs --unified`
- `list` -- list files in a directory tree; `--format text` prints one path per line and `total_bytes` counts files only. Example: `atomwrite --workspace "$WS" list --long --depth 1 src/`
- `extract` -- extract fields from NDJSON input via pipe (local `--field` or positionals; never `extract --select`, because the global `--select` only projects the emitted line). Example: `printf '{"a":1}\n' | atomwrite extract --field a`
- `extract-artifact` -- bounded signal from PDF, OOXML and image headers (never raw artifact bytes). Example: `atomwrite --workspace "$WS" extract-artifact img.png`
- `scope` -- grammatical scoping (delete all comments, and so on); `--language rust` or the alias `--lang rust`. Example: `atomwrite --workspace "$WS" scope src/ --language rust --query comments --delete --dry-run`
- `regex` -- generate a regex from examples. Example: `atomwrite regex abc-12 xyz-99 id-7 --digits`
- `calc` -- math and unit conversions. Example: `atomwrite calc "2 hours to seconds"`
- `completions` -- generate shell completions (bash, zsh, fish, elvish, powershell); `--install` writes them to the XDG data directory. Example: `atomwrite completions bash`

### Backup and recovery
- `backup` -- create timestamped backups with BLAKE3 checksums. Example: `atomwrite --workspace "$WS" backup src/a.rs --retention 3`
- `rollback` -- restore from a previous backup. Example: `atomwrite --workspace "$WS" rollback src/a.rs --latest --verify`
- `batch` -- NDJSON-driven batch operations (transactional with `--transaction`, which locks every target in path order before the snapshot). Example: `printf '{"op":"write","target":"batch.txt","content":"x"}\n' | atomwrite --workspace "$WS" batch --transaction`

### Batch helpers
- `edit-loop` -- N edits in 1 invocation via JSON array or NDJSON stdin (all-or-nothing: a pair with no match exits 65; `--partial` writes the matches and exits 87; the checksum read under the lock becomes the write base, so a concurrent edit exits 82). Example: `printf '[{"old":"gamma","new":"delta"}]\n' | atomwrite --workspace "$WS" edit-loop src/b.rs`
- `prune-backups` -- manual cleanup of legacy `.bak.*` files with `--max-age-secs` and `--max-count`. Example: `atomwrite --workspace "$WS" prune-backups -r --max-age-secs 86400 --dry-run .`

### WAL diagnostics
- `wal-stats` -- snapshot of the WAL journal state (counts, ages, sizes). Example: `atomwrite --workspace "$WS" wal-stats`
- `wal-heal` -- remove orphan terminal journals older than a threshold, and also remove a `Started` journal whose owning PID is gone, reported as `dead_owner_removed`; the path is required. Example: `atomwrite --workspace "$WS" wal-heal --threshold-secs 3600 .`

### Verification
- `verify` -- verify the BLAKE3 checksum of a file (exit 0 on match, 81 on mismatch), with the usage `verify <PATH> <HASH>`. Example: `atomwrite --workspace "$WS" verify src/a.rs "$HASH_A"`

### Structured config editors
- `set <PATH> <KEY_PATH> <VALUE>` -- write a value at a dotted path in a TOML or JSON file; preserves comments and key order via `toml_edit` and auto-coerces int, bool, float and string. Example: `atomwrite --workspace "$WS" set config.toml package.name demo2`
- `get <PATH> <KEY_PATH>` -- read a value at a dotted path; NDJSON `{"type":"get","key_path","value","found","format"}`. Example: `atomwrite --workspace "$WS" get config.toml package.name`
- `del <PATH> <KEY_PATH>` -- remove a key; `--force-missing` treats a missing key as a no-op success. Example: `atomwrite --workspace "$WS" del config.toml features.x`
- `case <PATHS...> --subvert OLD NEW --to <style>` -- rename identifiers across files and directories via `heck`; styles are `snake`, `camel`, `pascal`, `kebab` and `screaming-snake`. A directory expands through the shared walker and honours `--include` and `--exclude`, and a directory that yields no file exits 65. Example: `atomwrite --workspace "$WS" case --subvert user_id UserId --to pascal --dry-run src/c.rs`

### AST tools
- `query` -- walk a tree-sitter AST and emit nodes as NDJSON (372 languages in `tree-sitter-language-pack`), with the usage `query <PATH> [--kinds|--query <KIND>|-Q <KIND>|--tree] [--positions]`. Example: `atomwrite --workspace "$WS" query --kinds src/a.rs`
- `outline <PATH> [--kind <KIND>] [--positions] [--format tsv]` -- extract the high-level structure (functions, classes, structs, enums, traits, modules, Markdown headings) of one file as NDJSON or as `line<TAB>kind<TAB>name`; publishes `parse_errors` with `first_error_line`. Example: `atomwrite --workspace "$WS" outline src/a.rs`
- `write --syntax-check` runs a real tree-sitter syntax check for 23 languages and exits 88 with the first error line, column, kind and message

### Agent surface and workflows
- `recipe` -- list and run versioned multi-step pipelines such as search, replace and hash; `recipe run` needs `--path` or `--target`. Example: `atomwrite --workspace "$WS" recipe run --name search-replace-verify --pattern alpha --replacement omega --path src --dry-run`
- `sparse` -- budgeted monorepo list, read and outline (outline emits real AST `outline_item` events), and `--all` adds the hidden entries to the visit. Example: `atomwrite --workspace "$WS" sparse list src/ --max-files 10 --max-bytes 1048576 --all`
- `semantic-merge` -- line-based three-way merge for multi-agent conflicts (not AST); markers are on by default and a conflict exits 84. Example: `atomwrite --workspace "$WS" semantic-merge --base base.txt --ours ours.txt --theirs theirs.txt --output merged.txt --fail-on-conflict`
- `agent-surface` -- clap-derived tool manifest (CLI only; integration is by subprocess with NDJSON output). Example: `atomwrite agent-surface --format json`
- `watch` -- filesystem events with debounce, checksum and gitignore options; always ends with a `watch_summary` NDJSON line; needs a build with `--features watch`, and the default binary exits 78 with `CONFIG_INVALID`. Example: `atomwrite --workspace "$WS" watch src/ --max-events 1 --idle-exit-ms 500`
- `codemod` -- multi-rule AST campaign with a `by_rule_id` summary, driven by a YAML rules file (format below). Example: `atomwrite --workspace "$WS" codemod --rules rules.yaml --dry-run src/`
- `semantic-search` -- offline token ranking over an inverted index (always compiled; not embeddings). Example: `atomwrite --workspace "$WS" semantic-search "unwrap value" src/ --k 5 --min-score 0.01`
- `stat` -- alias of `read --stat`. Example: `atomwrite --workspace "$WS" stat src/a.rs`
- `doctor` -- host readiness diagnostics (workspace, config, features, concurrency); `--strict` exits 1 with `DOCTOR_CHECKS_FAILED` on a failed check. Example: `atomwrite --workspace "$WS" doctor`
- `locale` -- show the resolved UI locale; `--set` and `--clear` manage the XDG preference. Example: `atomwrite locale`
- `commands` -- the full clap command tree as JSON for agent discovery (`--include-globals` adds the 33 global flags, and `--brief` prints one line per subcommand). Example: `atomwrite commands --include-globals`
- `scratch` -- `scratch init PATH` creates the staging directory inside the workspace (never the OS `/tmp`). Example: `atomwrite --workspace "$WS" scratch init .atomwrite/scratch`
- `config` -- persistent tuning keys: `config path|list|get|set|unset|doctor` over 43 keys of the XDG `config.toml` and the workspace `.atomwrite.toml`; atomic write, envelope `type:"config"`. Example: `atomwrite --workspace "$WS" config get fuzzy.threshold`

### Write durability
- `write --durability full|fast|auto` sets the fsync trade-off, and the NDJSON reports `platform.durability`, `platform.rename_method` and `platform.backup_method`
- Linux uses `renameat2` when available and falls back to rename
- macOS uses `F_FULLFSYNC` under durability `full`

## Write Safety Flags

These flags guard destructive writes. All of them are opt-in except the large-overwrite and shrink guards, which are on by default.

- `--ack-overwrite` -- required to overwrite an existing target above `[write].confirm_large_bytes` (default 100 KiB); without it the write exits 65
- `--allow-shrink` -- required when the new content is more than 50% smaller than the target
- `--require-backup` -- takes no value; aborts when a backup would not be made while the target exists
- `--auto-rotate` -- takes no value; forces backup rotation for recently modified targets
- `--risk-threshold <PERCENT>` -- warns when the size delta passes the percent; the default is 255, which means off, and a value such as 50 turns it on
- `--confirm` on `write` is a legacy alias with no effect, and `edit` has no confirmation flag, so use `--dry-run` first
- `delete --confirm`, `--yes` and `-y` are rejected: use `delete --plan` to preview and omit the flags to run
- `--expect-checksum <HASH>` -- optimistic locking: the comparison runs under the lock and a mismatch exits 82 `STATE_DRIFT`, while exit 81 `CHECKSUM_VERIFY_FAILED` belongs to `verify` and `read`
- `--locale <en|pt-BR>` -- display locale override, renamed from `--lang` in 0.1.20 to avoid clashing with the language selector of `scope` and `transform`

## Backups And Sequential Edits
- A single `BackupOpts` set (`--backup`, `--no-backup`, `--keep-backup`, `--retention <N>`) is shared by 15 mutating subcommands: `write`, `edit`, `edit-loop`, `replace`, `transform`, `scope`, `apply`, `set`, `del`, `case`, `batch`, `delete`, `move`, `copy` and `rollback`
- Backup is on by default for the rewrite verbs: the backup is deleted after a successful write, kept when the write fails, and kept after success only with `--keep-backup`
- `delete` copies only when the call asks, through `--backup` or `[defaults].backup = true`; a copy it creates is never auto-removed, so `--keep-backup` on `delete` only adds a warning when no copy exists
- `--backup` and `--no-backup` are mutually exclusive (exit 2)
- `move` and `copy` overwrite an existing destination only with `--force` or an explicit `--backup`
- The `[defaults]` `retention` key of `.atomwrite.toml` applies to all 15 subcommands; `backup`, when written, governs the rewrite verbs and also reaches `delete` and the `batch` operations, while an absent key keeps the verb default (precedence: flags, then `.atomwrite.toml`, then the global `config.toml`, then the built-in `5`)
- `edit --allow-sequential-drift` suppresses `STATE_DRIFT` (exit 82) in a sequential pipeline and logs a warning
- Pattern A is to re-capture `checksum_after` after each `edit`, which has zero drift risk and is the right choice for concurrent agents
- Pattern B is to pass `--allow-sequential-drift` on every call, which needs fewer invocations but must never be used in a truly parallel scenario

## Output Contract

Every command writes NDJSON (one JSON object per line) to stdout, and stderr carries logs only. Query commands end with a summary event and write commands end with their own result event, so read the `type` of the last line. Schema files are in `docs/schemas/` (54 files serving 55 names, with `read` and `stat` sharing one; `atomwrite --print-schema` lists the 55 names). The contract is stable within the 0.1.x series. `--json` is accepted and ignored.

### Critical NDJSON fields
- `type` -- operation name (`set`, `get`, `del`, `case`, `query_match`, `outline_item`, `write`, `edit`, and so on)
- `ok` -- true on every success envelope, false on the terminal event of a failed run
- `checksum` (BLAKE3) -- the file content hash exposed by `read`, `write` and `hash`
- `checksum_before` and `checksum_after` (BLAKE3) -- exposed by `edit`: the hash before and after the operation
- `mtime_preserved` -- true if the mtime was kept, false if it was updated
- `bytes_written` -- total bytes written to the file
- `elapsed_ms` -- wall-clock time of the operation
- `target_resolved` and `target_source` -- the paths that ran, always an array even for one, and where it came from: `argv` when the invocation line named it, `workspace` when an idempotent read verb fell back to the root, and `stdin` for the `-` operand
- `workspace_source` -- `argv` when the run got its root from `--workspace`, and `cwd` when it took the process working directory, so the ambient authority is visible

### Error envelope
- A failed run ends with exactly one terminal event: `{"ok":false,"code":"...","exit":N,"path":"...","suggestion":"..."}`, and `message`, `error_class`, `retryable` and `suggestion_lang` appear only under `-v`
- `MATCH_FAILED` and `MATCH_AMBIGUOUS` (exit 65) add `best_candidate` and `match_count`
- Other optional fields are `missing_flag`, `conflict_count`, `pairs_total`, `pair_results`, `failed_pair_index`, `similar_paths` and `workspace`
- A cancelled run ends with `{"type":"cancelled","reason":"...","signal":"SIGTERM","deadline_exceeded":false}` and exit 124, 130 or 143
- Read the exit code before the stdout, retry only when `retryable` is true under `-v`, and follow `suggestion`
- Under `--select`, `--filter` and `--limit` the envelope of an `ok:false` failure is never hidden
- Every verb that walks a tree reports files it could not read or write in `files_failed`, `partial` and `errors[]` (at most `summary.errors_max_entries` entries (50 by default), and `path` is the marker `<walk>` when the walk failed without naming a file of its own): `search`, `replace`, `transform`, `case`, `scope`, `sparse`, `semantic-search` and `backup` keep exit 0 when they answered and some file failed, while `count`, `list`, `hash`, `copy`, `delete` and `prune-backups` fail the run over a partial tree, and a run with no match and a failure exits with the first failure (the contract is ADR-0077, `docs/decisions/0077-answer-family-and-walk-honesty.md`)
- `print-schema` names an error contract too: `atomwrite --print-schema error`

## Local Flags
- `--expect-checksum <HASH>` -- optimistic locking for concurrent edits
- `--include <PATTERN>` / `-g <PATTERN>` -- filter files by glob pattern
- `--stat` (read) -- skip the file content, metadata only
- `--format raw|ndjson` (read) -- select a byte stream or the NDJSON envelope
- `--grep <REGEX>` (read) -- return only lines matching a regex
- `--head N` / `--tail N` (read) -- first or last N lines
- `--lines START:END` (read) -- partial read with a 1-based inclusive range
- `--range START:END` (read) -- visible alias of `--lines`, and either flag repeats to read several windows in one call
- `--number` (read) -- prefix every printed line with `N:`, and `read.number = true` makes it the default
- `--symbol NAME` (read) -- print one function, class or Markdown section by name, listing both when the name repeats
- `--line N --enclosing` (read) -- print the smallest outline item that holds line N
- `--all` (read, apply, sparse) -- remove the read cap, replace every repeated SEARCH, or list hidden entries
- `--no-comments` (read) -- drop comment and blank lines before the window is counted; refused without `--number`
- `--pages A:B`, `--page N`, `--dpi N`, `--ocr`, `--max-tokens N`, `--crop X,Y,W,H`, `--high-res`, `--force` (read) -- PDF and picture shapes: page windows, render resolution, in-process OCR, a visual-token budget, a crop, the high-resolution tier and a resend
- `--show` (apply) -- return the changed lines with their 1-based numbers
- `--no-syntax-check` (apply) -- opt out of the parse before and after the swap
- `--format text` (search, list) -- one `path:line:col:text` per match, or one path per line
- `--format tsv` (outline) -- `line<TAB>kind<TAB>name` with the path printed once
- `--brief` (commands) -- one line per subcommand instead of the full tree
- `--by-size` (count) -- list the largest files
- `--no-begin-end` (search) -- disable anchor emission
- `--preserve-timestamps` (edit, replace, write) -- keep the original file mtime
- `--syntax-check` (write) -- real tree-sitter syntax check before commit
- `--positions` (query, outline) -- include `start_line`, `start_column` and `byte_offset`
- `--line-ending lf|crlf|cr|auto` (write) -- normalize line endings
- `--file <PATH>` (batch) -- read the NDJSON manifest from a file
- `--output-dir <DIR>` (backup) -- place backups in a custom directory
- `--install` (completions) -- install to the XDG data directory
- Advisory lock (automatic, no flag) -- `std::fs::File::try_lock` on the target's own inode, which is `flock` on Unix and `LockFileEx` on Windows, 5 s wait, exit 83 on timeout; the hidden `.<target>.atomwrite.lock` sidecar is best effort and its failure no longer aborts the write
- A target name long enough to push the sidecar past 255 bytes gets a short BLAKE3 digest as the sidecar name, so the lock is taken instead of failing with `ENAMETOOLONG`

## Global Flags

There are 33 global flags (`atomwrite commands --include-globals` prints them). `--print-schema` must come before the command name.

| Flag | Default | Meaning |
| --- | --- | --- |
| `--workspace <DIR>` | current directory | workspace root of the path jail, required for file operations |
| `--config <PATH>` | none | explicit `.atomwrite.toml` path, fails if missing or malformed |
| `--locale <en\|pt-BR>` | system | display locale override |
| `-v` / `-vv` / `-vvv` | off | info, debug and trace logs on stderr |
| `-q` / `-qq` | off | error-only and silent logs |
| `--color auto\|always\|never` | `auto` | colored output control |
| `--no-color` | off | disable colored output |
| `--ready-file <PATH>` | none | write the PID after the signal handlers are ready (test harness only) |
| `--no-gitignore` | off | do not respect `.gitignore` files |
| `--hidden` | off | include hidden files and directories |
| `--follow-symlinks` | off | follow symbolic links |
| `-j` / `--threads <N>` | automatic | worker bound (alias `--max-concurrency`): 0 or omitted is the smaller of the logical CPUs, 16 and the RAM cap, and an explicit N is honoured even above 16 |
| `--max-filesize <BYTES>` | 1 GiB | reject files larger than N bytes (`search` has its own local limit that skips) |
| `--timeout-secs <N>` | 120 | global deadline (alias `--timeout`), `0` disables, exit 124 on expiry |
| `--no-progress` | off | suppress NDJSON progress heartbeats |
| `--json-schema` | off | print the JSON Schema of the subcommand output and exit |
| `--print-schema [NAME]` | none | list the 55 schema names or emit one, an unknown name exits 65 |
| `--non-interactive` | off | declare agent execution: never prompt, never read a confirmation from stdin |
| `--confirm-side-effect` | off | acknowledge a side effect where the command contract asks for it |
| `--allow-truncate` | off | opt in to value truncation, required by `--max-output-bytes` |
| `--no-auto-heal` | off | skip the startup wal-heal pass (threshold 3600 s, budget 100 ms) |
| `--select KEYS` / `--fields KEYS` | none | project NDJSON to top-level keys (CSV) |
| `--filter key=value` | none | keep events by top-level equality (repeatable AND, also `!=`) |
| `--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 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 (long-only, `-n` belongs to `replace`) |
| `--sort-by path-line\|line-path\|KEY` | none | order buffered events |
| `--dedupe KEYS` | none | drop events whose key tuple was already seen |
| `--aggregate KEY[:OP[:FIELD]]` | none | group and reduce, OP is `count`, `sum`, `min` or `max` |
| `--join KEY:SUFFIX` | none | join the NDJSON table read from stdin into each event |
| `--truncate-content KEY:LEN` | none | truncate a string key to LEN characters |
| `--redact` | off | replace secret-shaped values with `[redacted]` |
| `--normalize-encoding` | off | normalize string values to Unicode NFC |
| `--diff-against PATH` | none | report added, removed and unchanged lines against a baseline NDJSON file (emits one `stream_diff` line) |
| `--max-output-bytes N` | none | cap every string value, `checksum` included, and requires `--allow-truncate` (each is rejected without the other, exit 2) |

## Configuration

`atomwrite config list` shows the 43 persistent keys with `value`, `origin` (`default`, `global` or `workspace`) and `default`. The global scope is the XDG `config.toml` (mode 0600) and the workspace scope is `.atomwrite.toml` at the workspace root. A flag still wins over both, and the two files are merged key by key.

| Key | Kind | Default | Scopes | Range |
| --- | --- | --- | --- | --- |
| `fuzzy.mode` | string | `auto` | global, workspace | `auto`, `aggressive`, `off` |
| `fuzzy.threshold` | float | 0.7 | global, workspace | 0.0 to 1.0 |
| `fuzzy.threshold_aggressive` | float | 0.5 | global, workspace | 0.0 to 1.0 |
| `fuzzy.threshold_context` | float | 0.8 | global, workspace | 0.0 to 1.0 |
| `fuzzy.threshold_jw` | float | 0.85 | global, workspace | 0.0 to 1.0 |
| `fuzzy.soft_floor_delta` | float | 0.05 | global, workspace | 0.0 to 1.0 |
| `fuzzy.max_content_bytes` | integer | 268435456 | global, workspace | bytes |
| `write.confirm_large_bytes` | integer | 102400 | global, workspace | bytes |
| `log.file` | boolean | false | global, workspace | true or false |
| `log.rotation` | string | `daily` | global, workspace | `daily` or `hourly` |
| `storage.home` | string | unset | global | a path |
| `locale` | string | unset | global | `en` or `pt-BR` |
| `concurrency.max_threads` | integer | 16 | global, workspace | 1 to 256 |
| `concurrency.ram_fraction_percent` | integer | 50 | global, workspace | 1 to 100 |
| `search.max_inflight_bytes` | integer | 8388608 | global, workspace | 4 MiB to 1 GiB |
| `lock.acquire_timeout_ms` | integer | 5000 | global, workspace | 100 to 3600000 |
| `retry.base_ms` | integer | 50 | global, workspace | 1 to 60000, not above `retry.max_ms` |
| `retry.max_ms` | integer | 400 | global, workspace | 1 to 60000 |
| `retry.max_elapsed_ms` | integer | 2000 | global, workspace | 1 to 3600000 |
| `extract.max_sample_rows` | integer | 10 | global, workspace | up to 100000 |
| `extract.max_text_chars` | integer | 400 | global, workspace | up to 10000000 |
| `extract.max_sheets` | integer | 32 | global, workspace | up to 10000 |
| `extract.max_slides` | integer | 64 | global, workspace | up to 10000 |
| `read.max_output_bytes` | integer | 24000 | global, workspace | 1 or more |
| `read.number` | boolean | `false` | global, workspace | `true`, `false` |
| `search.format` | string | `ndjson` | global, workspace | `ndjson`, `text` |
| `output.profile` | string | `full` | global, workspace | `full`, `lean` |
| `apply.max_ellipsis_lines` | integer | 200 | global, workspace | 1 or more |
| `delete.backup` | boolean | absent | global, workspace | `true`, `false` |

- `delete.backup` is absent by default and inherits `defaults.backup`, and `config get delete.backup` omits `value` while the key is not written
- Precedence: flags, then the workspace `.atomwrite.toml`, then the global `config.toml`, then the built-in defaults
- `config set KEY VALUE --scope global|workspace`, `config unset KEY --scope ...` and `config path --scope ...` need `--scope` (without it the exit is 2 and nothing is touched), while `config list` and `config get KEY` do not accept it
- `storage.home` and `locale` are global only: the workspace scope exits 65, while `log.file` and `log.rotation` are accepted in both scopes
- An unknown key or a value out of range exits 65 and writes nothing, while an unknown key or an invalid file inside a `config.toml` exits 78
- `set` keeps the comments of the file, validates the whole file and lands through tempfile, fsync and rename without a backup
- `config` works with a broken `config.toml`, and a file that is not valid TOML is never rewritten (`set` and `unset` exit 78)
- `defaults.line_ending`, `defaults.backup`, `defaults.retention` and `defaults.max_filesize` are `config` keys in both scopes, and the loader reads them from the same table
- Example: `atomwrite --workspace "$WS" config set fuzzy.threshold 0.9 --scope workspace`

## Codemod Rules File

`codemod --rules rules.yaml` takes a YAML list. Each rule has `language`, `pattern` and `rewrite`, and an optional `id` that tags the events and the `by_rule_id` summary.

```yaml
- id: unwrap-to-try
  language: rust
  pattern: '$E.unwrap()'
  rewrite: '$E?'
```

- `$NAME` matches one node and `$$$ARGS` matches several
- Run `codemod --dry-run` before you apply, and the same list works as `transform --rules rules.yaml`

## Exit Codes

The manifest of `atomwrite agent-surface --format json` lists 42 entries with 34 distinct exit numbers, and some numbers carry more than one `code`.

| Exit | Code | error_class | retryable |
| --- | --- | --- | --- |
| 0 | `SUCCESS` | none | no |
| 1 | `NO_MATCHES` (zero results) | permanent | no |
| 1 | `DOCTOR_CHECKS_FAILED` (`doctor --strict`) | precondition_failed | no |
| 2 | `ARGUMENT_PARSE_ERROR` | permanent | no |
| 2 | `UNSUPPORTED_COMBINATION` (flags that cannot run together) | permanent | no |
| 4 | `FILE_NOT_FOUND` | permanent | no |
| 4 | `GLOB_NO_MATCHES` (a glob operand matched nothing) | 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` (with `best_candidate`, also an `edit-loop` pair with no match) | permanent | no |
| 65 | `MATCH_AMBIGUOUS` (with `best_candidate`) | permanent | no |
| 65 | `BINARY_FILE` | precondition_failed | no |
| 65 | `FILE_TOO_LARGE` | precondition_failed | no |
| 69 | `GRAMMAR_UNAVAILABLE` (AST verb with no cached parser and no network) | precondition_failed | no |
| 73 | `CROSS_DEVICE` | conflict | yes |
| 74 | `IO_ERROR` | transient | yes |
| 78 | `CONFIG_INVALID` (unknown key, invalid file, newer `version`, or a verb whose feature is missing from the build) | permanent | no |
| 79 | `SYMLINK_BLOCKED` (symlink target blocked; `--follow-symlinks` resolves it) | precondition_failed | no |
| 80 | `TRANSACTION_ROLLBACK_FAILED` | permanent | no |
| 81 | `CHECKSUM_VERIFY_FAILED` | precondition_failed | no |
| 82 | `STATE_DRIFT` (optimistic locking failed) | conflict | yes |
| 83 | `LOCK_TIMEOUT` (automatic write lock, waits up to 5 s) | conflict | yes |
| 84 | `MERGE_CONFLICT` (semantic-merge) | permanent | no |
| 85 | `FIFO_DETECTED` | precondition_failed | no |
| 86 | `DEVICE_FILE` | precondition_failed | no |
| 87 | `EDIT_LOOP_PARTIAL` (`edit-loop --partial` wrote the matching pairs) | permanent | no |
| 88 | `SYNTAX_ERROR_DETECTED` | precondition_failed | no |
| 89 | `ALL_SKIPPED_BINARY` (every candidate file was 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` (global `--timeout-secs`, default 120, `0` disables) | 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` (the reader closed the pipe, the output is incomplete) | permanent | no |
| 143 | `SIGTERM` (clean cancel, no tempfile left) | none | no |
| 255 | `INTERNAL_ERROR` | permanent | no |

- Exit 65 also covers a missing `--ack-overwrite`, a file above `[fuzzy].max_content_bytes` under fuzzy, an invalid regex or glob, an invalid AST pattern or rules file, and an invalid `config` key or value
- A run cancelled by SIGTERM, SIGINT or the deadline leaves no WAL journal, while a crash (`kill -9`) still leaves one and `wal-heal` is the recovery

## Upgrading To 0.1.38

The three verbs an agent calls most spend fewer bytes, and four silent failures became exits a caller can branch on.

- `read` is bounded by `read.max_output_bytes` (24,000 bytes) and prints the exact next `--lines` on stderr, so `--all` is the only way to read a whole file past the cap
- `--number`, `--symbol`, `--line N --enclosing`, repeatable `--lines` and `--no-comments` are the cheap read shapes
- `search --format text`, `list --format text` and `outline --format tsv` are the compact shapes, and `search.format` picks the search default
- `apply` patches several files in one call, matches whole lines, refuses an ambiguous SEARCH unless `--all` is given, and runs the syntax check before and after the swap
- `target_resolved` is always an array, `error: true` left the failure envelope, and the prose fields of a failure appear only under `-v`
- The lock is taken on the target inode, and the sidecar is best effort
- `config` lists 43 keys, and `commands --brief` prints one line per subcommand
- The full migration list is in [MIGRATION](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/MIGRATION.md)


## Upgrading To 0.1.38 (read expansion)

`read` now accepts several targets, and this is additive for every caller that named one path.

- `read --format raw a.rs b.rs` and `read --format raw '*.rs'` concatenate byte for byte, exactly as `cat` does, with no header, no separator and no newline of its own
- Explicit paths are read in argv order, and a pattern is expanded by the binary against one level of its own directory, in `LC_ALL=C` byte order, skipping hidden names
- The batch copies in 64 KiB blocks and checks `--max-filesize` per target, so memory stays flat instead of holding the sum of the files
- A target that fails prints one line on stderr and the run continues, and the final status is the semantic code of the first failure (4 missing, 13 permission, 126 outside the `--workspace`), where `cat` would answer 1
- A `read --format raw` error now goes to stderr and never to stdout, because that channel promised bytes
- `--lines`, `--line`, `--context`, `--head`, `--tail`, `--grep` and `--no-comments` work on several targets in `--format raw` and print one `==> path <==` header per file, while `--stat` and `--verify-checksum` require a single target and exit 65 with more than one, and `read` with several targets without `--format raw` also exits 65
- `--format raw` stops at 24,000 bytes unless `--all` is passed, and prints on stderr the number of lines shown plus the `--lines` that reads the rest
- `--number` prints `N:text`, `--symbol NAME` prints one definition, and `--line N --enclosing` prints its block
- A pattern with `**` or with more than one separator exits 65 with its own message instead of matching something else in silence
- One target in `--format raw` and the default NDJSON format are byte-identical to 0.1.37
- The copy advances the cancel check per block, so `--timeout-secs` cuts a batch with exit 124


## Upgrading To 0.1.37 (agent contract)

These changes can break a consumer of the 0.1.36 contract.

- `watch` left the default feature set: the default binary exits 78 with `CONFIG_INVALID`, and `agent-surface` reports `available: false` with the install hint
- `edit-loop` is all-or-nothing: exit 65 means nothing was written, and `--partial` exits 87
- A failing summary (`search`, `replace`, `transform`, `edit-loop`, `semantic-merge`, `recipe`, `scope`) is the only error line of the run, and no second error envelope follows
- `apply` with a unified diff compares the context and the removed lines (shift up to 64 lines): a mismatch exits 65 `MATCH_FAILED` with `best_candidate` and writes nothing, where it used to write wrongly with exit 0
- `apply` takes several files in one call: omit the target and name each file with a bare path line above its first block, or send a unified diff whose header pairs name each file, and every file is checked before the first write
- `semantic-merge` with a conflict exits 84 `MERGE_CONFLICT`, not 65
- `case`, `transform`, `scope` and `codemod` fail a file that changed since it was read with `STATE_DRIFT` (82), so rerun the command
- A missing pair in `edit-loop` or in a multi-pair `edit` reports `MATCH_FAILED` instead of `INVALID_INPUT`
- An invalid regex or glob in any verb exits 65, not 255, and `doctor --strict` with a failed check exits 1, not 255
- `get`, `set` and `del` on an invalid TOML or JSON file exit 65, not 1
- `--max-output-bytes` requires `--allow-truncate`, and `null` fields are no longer emitted
- A block missing between similar lines under fuzzy is `MATCH_FAILED` and not `MATCH_AMBIGUOUS`
- `search.max_inflight_bytes` has a 4 MiB minimum, and a `.atomwrite.toml` that sets 1 MiB is rejected
- An unknown key or an invalid file in `config.toml` or `.atomwrite.toml` exits 78
- `replace`, `scope`, `codemod`, `case`, `delete`, `backup` and `prune-backups` need their paths, `wal-heal` and `scratch init` take the path as a positional, and `recipe run` needs `--path` or `--target`
- `--print-schema` must come before the command name, and an unknown name exits 65 with an error envelope
- `doctor` reads no environment variable and the `ci`, `github_actions` and `sudo` fields are gone from its report
- Full details and the step-by-step list are in `docs/MIGRATION.md`

## Environment, Signals And Privacy
- There are no product environment variables: use CLI flags, the global XDG `config.toml` and the workspace `.atomwrite.toml` only
- `doctor` reads no environment variable: host detection uses file probes only
- The CLI has no telemetry and never sends data anywhere
- The product starts two network uses: the parser download on the first use of an AST verb and the two OCR models on the first `read --ocr`, and both honour the proxy settings of the host
- A panic writes a local report file in the temporary directory through `human-panic`, and nothing is transmitted
- SIGPIPE is handled by a cooperative handler instead of the default signal action: the process cleans up (no lock, journal or tempfile stays) and exits 141, so recheck the state instead of assuming the work finished
- SIGTERM exits 143, SIGINT 130, SIGHUP 129 and the deadline 124, each after a clean shutdown
- The production code has 8 `unsafe` blocks, in `signal.rs`, `platform.rs` and `signal_handlers.inc.rs`

## Usage Examples

Every block below was run against a fresh workspace. The expected exit code is 0 unless the comment says otherwise.

```bash
# Write atomically with checksum
printf 'content\n' | atomwrite --workspace "$WS" write src/file.rs
```

```bash
# Write with mtime preservation
printf 'content\n' | atomwrite --workspace "$WS" write --preserve-timestamps src/file.rs
```

```bash
# Read with checksum
atomwrite --workspace "$WS" read src/file.rs
```

```bash
# Read raw bytes (no envelope)
atomwrite --workspace "$WS" read --format raw src/file.rs
```

```bash
# Optimistic locking: only write if the file has not changed
CS=$(atomwrite --workspace "$WS" read src/file.rs | jaq -r .checksum)
printf 'new_value\n' | atomwrite --workspace "$WS" write --expect-checksum "$CS" src/file.rs
```

```bash
# Safety flags: require a backup and warn above a 50 percent size delta
printf 'new_value\n' | atomwrite --workspace "$WS" write --require-backup --risk-threshold 50 src/file.rs
```

```bash
# Overwrite a target above the large-file threshold (a rewrite that shrinks the file by more than 50 percent also needs --allow-shrink)
printf 'new_value\n' | atomwrite --workspace "$WS" write --ack-overwrite src/file.rs
```

```bash
# Search in parallel
atomwrite --workspace "$WS" search 'unwrap' src/ --include '*.rs'
```

```bash
# Count the largest files
atomwrite --workspace "$WS" count --by-size --top 10 src/
```

```bash
# Edit with match_count and the new checksum
atomwrite --workspace "$WS" edit src/c.rs --old user_id --new account_id --replace-all --fuzzy auto | jaq '{match_count,indent_adjusted,checksum_after}'
```

```bash
# edit-loop --partial: the second pair misses, the first is written and the exit code is 87
printf '[{"old":"account_id","new":"user_id"},{"old":"nothing_here","new":"x"}]\n' | atomwrite --workspace "$WS" edit-loop --partial src/c.rs
```

```bash
# Stage a patch inside the jail, then edit
atomwrite --workspace "$WS" scratch init .atomwrite/scratch
SCR="$WS/.atomwrite/scratch"
printf %s 'user_id' >"$SCR/old.txt"
printf %s 'owner_id' >"$SCR/new.txt"
atomwrite --workspace "$WS" edit --old-file "$SCR/old.txt" --new-file "$SCR/new.txt" src/c.rs
```

```bash
# Agent-native shape: a missing text exits 65 and the envelope keeps only the chosen keys
atomwrite --workspace "$WS" --select ok,error,code,match_count,suggestion --filter ok=false --limit 1 edit --old missing --new x --fuzzy off src/b.rs
```

```bash
# Replace across files: preview first, then apply
atomwrite --workspace "$WS" replace --dry-run 'alpha' 'omega' src/
atomwrite --workspace "$WS" replace 'alpha' 'omega' src/
```

```bash
# AST transform via ast-grep
atomwrite --workspace "$WS" transform -p '$E.unwrap()' -r '$E?' -l rust src/
```

```bash
# Batch operations (transactional)
printf '{"op":"write","target":"batch.txt","content":"x"}\n' | atomwrite --workspace "$WS" batch --transaction
```

```bash
# Math, units and regex generation
atomwrite calc "2 GiB to bytes"
atomwrite regex "2024-01-15" "2025-12-31" "2026-07-04"
```

```bash
# Structured editors and case renaming
atomwrite --workspace "$WS" set config.toml package.version 0.2.0
atomwrite --workspace "$WS" get config.toml package.version
atomwrite --workspace "$WS" case --subvert user_id UserId --to pascal src/c.rs
```

```bash
# Walk the AST and read the outline
atomwrite --workspace "$WS" query --kinds src/a.rs
atomwrite --workspace "$WS" outline src/a.rs --kind function_item
```

```bash
# Real syntax check before commit
printf 'fn main() {}\n' | atomwrite --workspace "$WS" write --syntax-check src/parser.rs
```

```bash
# Backup, then roll back
atomwrite --workspace "$WS" backup src/a.rs
atomwrite --workspace "$WS" rollback src/a.rs --latest --verify
```

```bash
# Apply a unified diff from stdin
printf -- '--- a/notes.txt\n+++ b/notes.txt\n@@ -1,2 +1,2 @@\n-one\n+uno\n two\n' | atomwrite --workspace "$WS" apply notes.txt
```

```bash
# Plan a delete: a directory needs -r even for a plan
atomwrite --workspace "$WS" delete --plan -r src/
```

```bash
# Persistent tuning keys
atomwrite --workspace "$WS" config list
atomwrite --workspace "$WS" config set fuzzy.threshold 0.9 --scope workspace
```

```bash
# Agent discovery
atomwrite --workspace "$WS" doctor --strict
atomwrite commands --include-globals
atomwrite agent-surface --format json
atomwrite locale
```

```bash
# Codemod: preview the rules file above
atomwrite --workspace "$WS" codemod --rules rules.yaml --dry-run src/
```

```bash
# Stream shape: sort and count without jq
atomwrite --workspace "$WS" --sort-by path-line --aggregate type:count list src/ --depth 1
```

```bash
# Continuous watch with a ceiling: exits 78 on a build without the watch feature
atomwrite --workspace "$WS" watch src/ --max-events 100 --idle-exit-ms 500 --checksum
```

## Primary Documentation
- [README](https://github.com/danilo-aguiar-br/atomwrite/blob/main/README.md): full documentation with all 47 subcommands, every local flag per subcommand and examples
- [HOW_TO_USE](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/HOW_TO_USE.md): quickstart guide
- [COOKBOOK](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/COOKBOOK.md): recipes for common tasks
- [AGENTS](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/AGENTS.md): agent discovery file with the output contract and the token budget
- [INTEGRATIONS](https://github.com/danilo-aguiar-br/atomwrite/blob/main/INTEGRATIONS.md): integration patterns for agents and scripts
- [ARCHITECTURE](https://github.com/danilo-aguiar-br/atomwrite/blob/main/ARCHITECTURE.md): write path, jail and WAL design
- [INSTALL](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/INSTALL.md): platform-specific install instructions
- [docs/schemas/](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/schemas/README.md): 55 served names backed by 54 files under `docs/schemas/`
- [docs/decisions/](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/decisions/): 77 ADRs (0019 to 0095)

## Optional
- [TESTING](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/TESTING.md): 1486 tests passed in 131 test targets at the 0.1.38 release gate
- [MIGRATION](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/MIGRATION.md): migration guide between versions
- [CROSS_PLATFORM](https://github.com/danilo-aguiar-br/atomwrite/blob/main/docs/CROSS_PLATFORM.md): macOS, Linux and Windows quirks
- [CHANGELOG](https://github.com/danilo-aguiar-br/atomwrite/blob/main/CHANGELOG.md): release history
- [PRIVACY](https://github.com/danilo-aguiar-br/atomwrite/blob/main/PRIVACY.md): what the CLI reads, writes and never sends
- [SECURITY](https://github.com/danilo-aguiar-br/atomwrite/blob/main/SECURITY.md): vulnerability reporting
- [CONTRIBUTING](https://github.com/danilo-aguiar-br/atomwrite/blob/main/CONTRIBUTING.md): contributor onboarding, local definition of done and release process
- [CODE_OF_CONDUCT](https://github.com/danilo-aguiar-br/atomwrite/blob/main/CODE_OF_CONDUCT.md): Contributor Covenant 2.1 community standards
- [Crates.io](https://crates.io/crates/atomwrite): package registry
- [Repository](https://github.com/danilo-aguiar-br/atomwrite): source code