atomwrite 0.1.37

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
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
# How to Use atomwrite


[Leia em Português](HOW_TO_USE.pt-BR.md)

- One CLI replaces dozens of file-manipulation tool calls your agent makes today


## Reading and Running Time
- Reading time is about 45 minutes for the whole guide (about 10000 words 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.37 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]COOKBOOK.md holds longer recipes


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


## Prerequisites
- Rust toolchain 1.90 or later (edition 2024)
- Linux, macOS and Windows are supported
- Install from crates.io and confirm the version

```bash
cargo install atomwrite --locked --version "^0.1.37"
atomwrite --version
```

- Expect a line that starts with `atomwrite 0.1.37`
- 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]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

```bash
mkdir -p demo && cd demo
WS="$(pwd)"
printf 'hello world\n' | atomwrite --workspace "$WS" write src/hello.txt
```

- Expect one NDJSON line on stdout with `type` set to `write` and `ok` set to `true`
- The line carries `path`, `bytes_written`, `checksum`, `elapsed_ms`, `wal_policy` and `platform`
- It also carries `target_resolved` and `target_source`, which prove which path was written

```json
{"type":"write","status":"success","path":"src/hello.txt","bytes_written":12,"checksum":"dc5a4edb8240b018124052c330270696f96771a63b45250a5c17d3000e823355","ok":true,"target_source":"argv"}
```

- This is an excerpt, and the real line has more fields
- Read it back with metadata and checksum

```bash
atomwrite --workspace "$WS" read src/hello.txt
```

- Expect `type` `read` with `content`, `lines`, `bytes`, `checksum`, `permissions`, `modified` and `ok`
- Stdout is always NDJSON, stderr only carries diagnostics, and the exit code comes first
- Every write survives a crash, because it goes through tempfile, fsync and rename


## Core Commands
- Each command has a summary, its main flags, one tested example and the expected output
- Examples share one sample workspace, so run them in order inside `demo`

### Sample Workspace
- Create the files that the next examples use

```bash
mkdir -p src tmp
printf 'fn main() {\n    // TODO: wire config\n    let v = load().unwrap();\n    println!("{}", v);\n}\n\nfn load() -> Option<i32> {\n    Some(1)\n}\n' > src/main.rs
printf 'pub fn add(a: i32, b: i32) -> i32 {\n    a + b\n}\n' > src/lib.rs
printf 'fn alpha() {}\n' > src/a.rs
printf 'fn beta() {}\n' > src/b.rs
printf '[package]\nname = "demo"\nversion = "0.1.0"\n\n[database.pool]\nmax = 4\n' > Cargo.toml
```

### write
- Creates or overwrites a file atomically from stdin, in the order tempfile, fsync, rename, fsync-dir
- Your data reaches disk or the operation fails cleanly, and success carries a BLAKE3 `checksum`

```bash
printf 'fn main() {}\n' | atomwrite --workspace "$WS" write src/scratch.rs
printf 'longer content for the file\n' | atomwrite --workspace "$WS" write --backup --keep-backup src/hello.txt
printf 'checked write\n' | atomwrite --workspace "$WS" write --dry-run src/dry.txt
```

- Expect `type` `write` for the first two and `type` `plan` with `would_modify` for the dry run
- A backup is made by default and removed after success, and `--keep-backup` keeps the `.bak.<timestamp>` sibling
- A failed write always keeps its backup, `--no-backup` turns backups off and `--backup` with `--no-backup` exits 2
- `--expect-checksum HASH` writes only if the current checksum matches, or exits 82 (`STATE_DRIFT`)

```bash
CS="$(atomwrite --workspace "$WS" hash src/hello.txt | jaq -r .checksum)"
printf 'longer content, checked write\n' | atomwrite --workspace "$WS" write --expect-checksum "$CS" src/hello.txt
```

- `--append` and `--prepend` add to the file, and `--line-ending lf|crlf|cr|auto` normalizes endings
- `--durability full|fast|auto` picks the fsync strength, and the default `auto` uses full only for config files
- `--wal-policy auto|always|never` controls the journal sidecar, and `always` forces it even for trivial writes
- `--syntax-check` parses the result with tree-sitter and exits 88 (`SYNTAX_ERROR_DETECTED`) on error

```bash
printf 'fn ok() {}\n' | atomwrite --workspace "$WS" write --syntax-check src/ok.rs
printf 'tail line\n' | atomwrite --workspace "$WS" write --append src/hello.txt
```

- A rewrite that shrinks the file by more than 50% needs `--allow-shrink`, or exits 65
- An existing target above 100 KiB (`[write].confirm_large_bytes`) needs `--ack-overwrite`, or exits 65
- Empty stdin exits 65 unless you pass `--allow-empty-stdin`
- `--preserve-timestamps` keeps the old mtime, and by default mtime is updated so build tools rebuild
- Other flags are `--max-size`, `--require-backup`, `--auto-rotate`, `--risk-threshold PERCENT` and `--retention N`
- `--require-backup` and `--auto-rotate` take no value, and `--risk-threshold` is a percentage with default 255 (off)
- Success reports `platform.durability`, `platform.rename_method` and `platform.backup_method`
- A SIGTERM during a write removes the tempfile and exits 143

### read
- Reads a file with `content`, `lines`, `bytes`, `checksum`, `permissions` and `modified`
- The event also carries `target_resolved` and `target_source`, the same pair the writing verbs publish, so the agent confirms the path it read
- Binary files are detected, and their content is omitted

```bash
atomwrite --workspace "$WS" read --stat src/main.rs
atomwrite --workspace "$WS" read --lines 1:3 src/main.rs
atomwrite --workspace "$WS" read --head 2 src/main.rs
atomwrite --workspace "$WS" read --tail 2 src/main.rs
atomwrite --workspace "$WS" read --line 3 --context 1 src/main.rs
atomwrite --workspace "$WS" read --grep unwrap src/main.rs
atomwrite --workspace "$WS" read --format raw --head 2 src/main.rs
atomwrite --workspace "$WS" read --format raw src/main.rs src/cli.rs
atomwrite --workspace "$WS" read --format raw 'src/*.rs'
```

- Expect `type` `read` with `mode` `stat` 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`
- 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

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

```bash
atomwrite --workspace "$WS" edit src/main.rs --old 'wire config' --new 'wired config'
atomwrite --workspace "$WS" edit src/a.rs --old 'alpha' --new 'alpha_one' --old '{}' --new '{ 1 }'
printf '    // inserted\n' | atomwrite --workspace "$WS" edit src/main.rs --after-line 1
printf 'one\ntwo\nthree\nfour\n' | atomwrite --workspace "$WS" write tmp/five.txt
printf 'TWO\n' | atomwrite --workspace "$WS" edit tmp/five.txt --range 2:2
atomwrite --workspace "$WS" edit tmp/five.txt --delete-range 1:1
```

- Expect `type` `edit`, and repeated `--old`/`--new` pairs add `pairs_total` and `pair_results`
- Position modes read the new text from stdin: `--after-line`, `--before-line`, `--range`, `--after-match`, `--before-match`, `--between`
- A terminal on stdin makes those modes exit 65 at once instead of waiting
- `--delete-range N:M` needs no stdin, and `--multi` applies several NDJSON edits in one write
- `--multi` reads one JSON object per line from stdin, never a JSON array, and applies them all in a single atomic write
- Each object names its `op`: `exact`, `insert-after`, `insert-before`, `replace-range` or `delete-range`
- The content field follows the op: `content` for the three line-insert and range-replace ops, `old` and `new` for `exact`, and nothing for `delete-range`
- A field the chosen op does not read is exit 65 and nothing is written, so a stray `new` can never become a silent delete
- Line numbers refer to the ORIGINAL file, and two operations that touch the same line are refused before anything is applied

```bash
printf "%s\n" \
  '{"op":"replace-range","start":2,"end":2,"content":"TWO"}' \
  '{"op":"insert-after","line":4,"content":"five"}' \
  '{"op":"delete-range","start":6,"end":6}' \
  | atomwrite --workspace "$WS" edit tmp/lines.txt --multi
```

- The example replaces line 2, inserts after line 4 and deletes line 6, all against the same original numbering
- `--fuzzy auto|aggressive|off` handles near matches, and `off` means exact-only, with `auto` as the default
- `--fuzzy-threshold 0.0-1.0` tunes the cascade, and a result may carry `fuzzy`, `strategy` and `similarity`
- A miss exits 65 with `MATCH_FAILED`, `match_count` 0 and, when one exists, a `best_candidate`
- Several matches need `--replace-all`, and the success line then reports `match_count`
- A realigned indent adds `indent_adjusted: true`
- `--partial` applies the pairs that match and reports the rest, with exit 1 when no pair applied
- `--expect-checksum HASH` guards against drift, and `--allow-sequential-drift` accepts a chain of your own edits
- `--old-file` and `--new-file` read text from files that live inside the workspace (see Advanced Patterns)
- Never pipe `edit` into `jaq` without checking `${PIPESTATUS[0]}`, because the error envelope also goes to stdout

### search
- Searches file contents in parallel with the ripgrep engine
- Each file gets a `begin` event, its `match` events and an `end` event, then one `summary` closes the run

```bash
atomwrite --workspace "$WS" search 'TODO' src/
atomwrite --workspace "$WS" search --regex 'fn\s+\w+' src/
atomwrite --workspace "$WS" search --count 'fn' src/
atomwrite --workspace "$WS" search --files 'fn' src/
atomwrite --workspace "$WS" --filter type=match search -F 'println!' src/
atomwrite --workspace "$WS" search --target files lib src/
```

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

```bash
atomwrite --workspace "$WS" replace --dry-run alpha_one omega src/
atomwrite --workspace "$WS" replace --preview alpha_one omega src/
atomwrite --workspace "$WS" replace alpha_one omega src/a.rs
atomwrite --workspace "$WS" replace --fuzzy auto --fuzzy-threshold 0.85 --progress-every 25 --dry-run omega alpha_one src/
```

- Expect `type` `plan` per file on a dry run, `type` `preview` with a diff, and `type` `replaced` on a write
- Every run ends in a `summary` with `files_modified` and `total_replacements`
- Zero matches exit 1 with `NO_MATCHES`, and a trailing summary carries `ok: false`
- `--dry-run` and `--preview` show the plan, so use them before any bulk change
- `--literal` (`-F`) turns regex off, `--word` (`-w`) matches whole words and `--preserve-case` adapts the case
- `--max-replacements N` (`-n`) bounds the fuzzy applies, with a default of 1 when omitted and a hard ceiling of 10000
- Fuzzy applies are one-pass from left to right, and inserted text is never scanned again
- When the new text contains the old text, a single apply is forced, so expanding a section is safe
- `--include`, `--exclude`, `--expect-checksum` and `--preserve-timestamps` work as in `edit`
- `--progress-every N` emits progress heartbeats on large trees, and `--no-progress` silences them
- A successful `replace` deletes its `.bak` like every other mutator, and `--keep-backup` is the only way to keep it


## Utility Commands

### hash
- Computes the BLAKE3 checksum of files or of stdin
- Read the field `checksum`, and never the old field `value`

```bash
atomwrite --workspace "$WS" hash src/main.rs
printf 'data\n' | atomwrite --workspace "$WS" hash --stdin
atomwrite --workspace "$WS" hash --recursive --exclude '*.bak*' src/
H="$(atomwrite --workspace "$WS" hash src/lib.rs | jaq -r .checksum)"
atomwrite --workspace "$WS" hash --verify "$H" src/lib.rs
```

- Expect `type` `hash` with `algorithm` `blake3`, `checksum`, `bytes` and `ok`
- `--verify HASH` answers with `type` `hash_verify` and `verified`, and exits 81 on a mismatch

### delete
- Deletes files or trees, keeps a `.bak.<timestamp>` backup and can print only the plan
- Deleting never asks for confirmation, so use `--plan` first

```bash
printf 'x' > tmp/old.txt
atomwrite --workspace "$WS" delete --plan tmp/old.txt
atomwrite --workspace "$WS" delete tmp/old.txt
mkdir -p tmp/d && printf '1\n' > tmp/d/f
atomwrite --workspace "$WS" delete --recursive tmp/d
```

- Expect `type` `plan` with `would_modify`, then `type` `deleted` with `checksum_before`, then a `summary`
- A directory without `--recursive` (`-r`) exits 65 and names the missing flag
- `--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` skips the backup
- `delete --confirm` and `delete --yes` (`-y`) are rejected with exit 65, so run again without them
- A delete backup is always kept, so clear it with `prune-backups`

### count
- Counts lines, blank lines and bytes, by extension or by size

```bash
atomwrite --workspace "$WS" count src/
atomwrite --workspace "$WS" count --by-extension src/
atomwrite --workspace "$WS" count --by-size --top 3 src/
```

- Expect `type` `count` with `mode` `lines`, `by_extension` or `by_size`
- `--include` (`-g`) and `--exclude` filter the walk

### diff
- Compares two files, with unified, stat or change-by-change output
- A path of `-` reads that side from stdin

```bash
atomwrite --workspace "$WS" diff src/a.rs src/b.rs
atomwrite --workspace "$WS" diff --stat src/a.rs src/b.rs
atomwrite --workspace "$WS" diff --unified --context 5 src/a.rs src/b.rs
atomwrite --workspace "$WS" diff --algorithm patience src/a.rs src/b.rs
```

- Expect `change` events and a closing `summary` with `identical` and `similarity_ratio`
- `--stat` emits `type` `diff` with `insertions` and `deletions`, and `--unified` emits the patch in `content`
- `--algorithm` takes `myers`, `patience` or `lcs`

### move
- Moves or renames a file or directory atomically
- Across devices it falls back to copy and delete, and reports `cross_device`

```bash
atomwrite --workspace "$WS" copy src/lib.rs src/lib_copy.rs
atomwrite --workspace "$WS" move src/lib_copy.rs src/lib_moved.rs
```

- Expect `type` `moved` with `source`, `target`, `checksum`, `cross_device` and `atomic`
- An existing target needs `--force` (`-f`) or `--backup`, or the call exits 65
- `--preserve-hardlinks` keeps hard links, and `--dry-run` shows the plan

### copy
- Copies files or trees and verifies the BLAKE3 checksum after the write

```bash
atomwrite --workspace "$WS" copy --force src/lib.rs src/lib_copy.rs
atomwrite --workspace "$WS" copy --recursive --preserve src/ tmp/srccopy
```

- Expect `type` `copied` with `checksum` and `verified` set to `true`
- `--recursive` (`-r`) copies trees, `--preserve` keeps metadata and `--preserve-xattr` keeps extended attributes
- `--no-reflink` forces a byte copy, and reflink is the default where the filesystem supports it
- Backups use `reflink_or_copy` and never a hard link of the live file

### list
- Lists a directory tree with one NDJSON event per entry

```bash
atomwrite --workspace "$WS" list --long --depth 1 src/
atomwrite --workspace "$WS" list --count-by-ext src/
atomwrite --workspace "$WS" list --include '*.rs' --depth 1 src/
```

- Expect `type` `entry` with `path` and `kind`, plus `size` and `modified` with `--long`
- The closing `summary` has `files`, `dirs`, `symlinks` and `total_bytes`
- `--all` includes hidden entries, and `--exclude` filters by glob

### extract
- Picks fields from NDJSON read on stdin, or columns from plain text
- Use positional keys or repeatable `--field`, and never both in one call
- `--select` is a global flag, so `extract --select` is not valid

```bash
printf 'a b c\n' | atomwrite extract 0 2
printf '{"a":1,"b":2}\n' | atomwrite extract --field a
atomwrite --workspace "$WS" search 'TODO' src/ | atomwrite extract --field path --field line_number
printf '{"a":1,"b":2}\n' | atomwrite --select a extract --field a
```

- Expect `{"type":"text","values":["a","c"]}` for text columns and `{"a":1}` for the NDJSON case
- `--delimiter` (`-d`) sets the column separator, and `--stdin` reads the input stream

### extract-artifact
- Extracts bounded signal from PDF, OOXML (documents, sheets, slides) and image headers
- It never returns raw artifact bytes

```bash
printf '\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\001\000\000\000\001\010\006\000\000\000\037\025\304\211' > img.png
atomwrite --workspace "$WS" extract-artifact img.png
```

- Expect `type` `artifact` with `kind` `image`, `format` `png`, `width` and `height`
- `--max-sample-rows` and `--max-text-chars` override the `extract.*` config keys

### calc
- Evaluates math and unit conversions with the fend engine

```bash
atomwrite calc "2 hours + 30 minutes to seconds"
atomwrite calc "sqrt(144) + 3^2"
atomwrite calc "10 GiB to MB"
```

- Expect `type` `calc` with `expression` and `result`, such as `9000 seconds`
- Quote the expression, because the shell would split it, and `--stdin` reads it from input

### regex
- Generates a regular expression from example strings, with the grex engine

```bash
atomwrite regex "2024-01-15" "2025-12-31" "2026-06-01"
atomwrite regex abc-12 xyz-99 id-7 --digits
atomwrite regex --digits --words "user_123" "admin_456"
```

- Expect `type` `regex` with `regex`, `examples` and `anchored`
- `--digits` (`-d`), `--words` (`-w`), `--spaces` (`-s`), `--repetitions` (`-r`) and `--case-insensitive` (`-i`) widen the pattern
- `--no-anchors` drops `^` and `$`, and `--stdin` reads examples from input


## Advanced Commands

### scope
- Selects code by grammar and acts on it, with `--delete`, `--replace-with` or `--action`
- It needs `--language` (`-l`, alias `--lang`) and exactly one of `--query` or `--pattern`

```bash
atomwrite --workspace "$WS" scope src/main.rs --language rust --query comments --delete --dry-run
atomwrite --workspace "$WS" scope --query fn --action upper -l rust --dry-run src/lib.rs
atomwrite --workspace "$WS" scope --pattern 'a + b' --action upper -l rust --dry-run src/lib.rs
```

- Expect `type` `scoped` with `scopes_matched`, `bytes_before` and `bytes_after`, then a `summary`
- The summary reports `total_matches` as the sum of `scopes_matched` across the run, beside `files_visited` and `files_matched`
- `files_matched` counts the files that matched something, and a file that failed or was skipped never counts
- `--action` takes `upper`, `lower`, `titlecase`, `squeeze`, `symbols` and `normalize`
- Queries include `comments`, `strings`, `fn`, `pub-fn`, `async-fn`, `struct`, `enum`, `trait`, `impl`, `mod` and `use`
- Always run `--dry-run` before `--delete`, and remove the flag to apply

### transform
- Searches and rewrites code by AST pattern with the ast-grep engine
- `$NAME` captures one node, `$$$ARGS` captures several, and `-p` and `-r` are used together

```bash
atomwrite --workspace "$WS" transform --dry-run -p '$E.unwrap()' -r '$E?' -l rust src/
atomwrite --workspace "$WS" transform --dry-run -p 'println!($$$ARGS)' -r 'tracing::info!($$$ARGS)' -l rust src/
```

- Expect `type` `transformed` 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

```bash
printf '%s\n' '{"op":"write","target":"batch.txt","content":"x"}' '{"op":"hash","target":"batch.txt"}' | atomwrite --workspace "$WS" batch --transaction
printf '%s\n' '{"op":"write","target":"batch.txt","content":"y"}' | atomwrite --workspace "$WS" batch --dry-run
```

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

```bash
printf 'new body\n' | atomwrite --workspace "$WS" apply --format full tmp/applied.txt
printf -- '--- a/src/b.rs\n+++ b/src/b.rs\n@@ -1 +1 @@\n-fn beta() {}\n+fn beta_v2() {}\n' > tmp/fix.patch
atomwrite --workspace "$WS" apply --dry-run src/b.rs < tmp/fix.patch
atomwrite --workspace "$WS" apply src/b.rs < tmp/fix.patch
```

- Expect `type` `plan` on the dry run and `type` `applied` with `format_detected` and `hunks_applied`
- 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

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

```bash
atomwrite --workspace "$WS" set Cargo.toml package.version 0.2.0
atomwrite --workspace "$WS" set Cargo.toml database.pool.max 9
printf '{"scripts":{"build":"tsc"}}\n' > package.json
atomwrite --workspace "$WS" set package.json scripts.build "tsc -b"
```

- Expect `type` `set` with `key_path`, `old_value`, `new_value`, `format` and `comments_preserved`
- `set` accepts the backup flags and `--preserve-timestamps`, and it needs no type flag
- Use `config set` for the tuning keys of atomwrite itself

### get
- Reads a value at a dotted path in a TOML or JSON file

```bash
atomwrite --workspace "$WS" get Cargo.toml package.version
atomwrite --workspace "$WS" get package.json scripts.build
```

- Expect `type` `get` with `value`, `found` and `format`
- A missing key exits 65 with `INVALID_INPUT`, and it does not return `found: false`
- A file that is not valid TOML or JSON also exits 65

### del
- Removes a key at a dotted path in a TOML or JSON file

```bash
atomwrite --workspace "$WS" del Cargo.toml database.pool.max
atomwrite --workspace "$WS" del --force-missing Cargo.toml package.metadata.deprecated
```

- Expect `type` `del` with `removed_value` and `existed`
- `--force-missing` turns a missing key into a success with `existed` set to `false`

### case
- Renames an identifier across source files and converts the new name to a case style
- `--to` takes `snake` (default), `camel`, `pascal`, `kebab` and `screaming-snake`
- Pass files, because a directory exits 0 and changes nothing (`identifiers_total` is 0)

```bash
printf 'fn user_id() {}\nlet userId = 1;\n' > src/c.rs
atomwrite --workspace "$WS" case --subvert user_id account_id --to camel --dry-run src/c.rs
atomwrite --workspace "$WS" case --subvert user_id account_id --to camel src/c.rs
```

- Expect `type` `case_preview` on the dry run and `type` `case` with `before` and `after` on a write
- A closing `summary` reports `identifiers_total` and `files_modified`
- Name several files to rename across a module, and run `--dry-run` first on a large tree

### query
- Walks the tree-sitter AST of a file and emits nodes as NDJSON

```bash
atomwrite --workspace "$WS" query --kinds src/lib.rs
atomwrite --workspace "$WS" query --query function_item --positions src/lib.rs
atomwrite --workspace "$WS" query --tree src/lib.rs
```

- Expect `query_kind`, `query_match` or `query_summary` events with `kind`, `text` and byte offsets
- `--query KIND` (`-Q`) filters by node kind, `--kinds` lists kinds and `--tree` lists every node
- `--positions` adds line and column, and `--language` sets the language when the extension is unclear
- An invalid query exits 65, and a slim build without `ast` exits 78

### outline
- Emits the high-level structure of a source file: functions, structs, enums, traits and modules
- It needs a file, and a directory is not accepted

```bash
atomwrite --workspace "$WS" outline src/lib.rs
atomwrite --workspace "$WS" outline --kind function_item --positions src/lib.rs
```

- Expect `outline_item` events with `kind`, `name`, `signature`, `start_line` and `end_line`, then an `outline_summary`
- `--kind` is repeatable, and `--positions` adds bytes and columns
- A slim build without `ast` exits 78 with a rebuild hint


## Safety and Maintenance Commands

### backup
- Creates a timestamped `.bak.<timestamp>` copy of a file with a BLAKE3 checksum

```bash
atomwrite --workspace "$WS" backup src/lib.rs --retention 3
atomwrite --workspace "$WS" backup --dry-run src/lib.rs
```

- Expect `type` `backup` with `backup_path` and `checksum`, then a `summary` with `files_backed_up`
- `--retention N` keeps the N newest backups, `--output-dir DIR` writes elsewhere and `--dry-run` previews

### rollback
- Restores a file from a previous backup, and `--verify` checks the BLAKE3 checksum first

```bash
atomwrite --workspace "$WS" rollback src/lib.rs --latest --verify --dry-run
atomwrite --workspace "$WS" rollback src/lib.rs --latest --verify
```

- Expect `type` `plan` on the dry run and `type` `rollback` with `restored_from` and `verified`
- `--timestamp STAMP` restores a specific backup, and a prefix of the stamp is accepted
- The pre-rollback safety snapshot is opt-in through `--backup`

### verify
- Checks a file against an expected BLAKE3 hash, with exit 0 on a match and 81 on a mismatch
- The hash is positional, so the call is `verify PATH HASH`

```bash
H="$(atomwrite --workspace "$WS" hash src/lib.rs | jaq -r .checksum)"
atomwrite --workspace "$WS" verify src/lib.rs "$H"
```

- Expect `type` `hash_verify` with `verified` set to `true` and `ok` set to `true`
- On a mismatch the same event carries `verified: false`, `code` `CHECKSUM_VERIFY_FAILED` and `exit` 81

### edit-loop
- Applies N old/new pairs to one file in a single write, all or nothing
- Send a JSON array or one JSON object per line on stdin

```bash
printf 'alpha\nbeta\ngamma\n' > tmp/loop.txt
printf '[{"old":"alpha","new":"ALPHA"},{"old":"beta","new":"BETA"}]\n' | atomwrite --workspace "$WS" edit-loop tmp/loop.txt
printf '{"old":"gamma","new":"GAMMA"}\n{"old":"absent","new":"X"}\n' | atomwrite --workspace "$WS" edit-loop --partial tmp/loop.txt
```

- Expect `type` `result` with `pairs_total`, `pairs_applied`, `pairs_unmatched` and `pair_results`
- Without `--partial`, an unmatched pair exits 65 with `MATCH_FAILED` and `failed_pair_index`, and nothing is written
- With `--partial`, matching pairs are written and the run exits 87 (`EDIT_LOOP_PARTIAL`) when any pair missed
- `--syntax-check`, `--line-ending` and `--allow-sequential-drift` are also accepted

### prune-backups
- Removes `.bak` siblings of the named target files by age or by count
- Name the target files, not a directory, and pass `--max-age-secs` or `--max-count`
- `--dry-run` is a flag without a value, so omit it to apply

```bash
atomwrite --workspace "$WS" prune-backups --max-age-secs 0 --dry-run src/hello.txt
atomwrite --workspace "$WS" prune-backups --max-age-secs 0 src/hello.txt
atomwrite --workspace "$WS" prune-backups --max-count 3 src/lib.rs
```

- Expect `pruned` events with `reason` `age` or `count`, then a `summary` whose `action` is `dry_run` or `pruned`
- Only names carrying a real backup stamp (`.bak.YYYYMMDD_HHMMSS` or `.bak.YYYYMMDD_HHMMSS_mmm`) are candidates, so `f.txt.bak.0-user-note` is never touched
- `--dry-run false` does not apply anything, because `false` is read as a path

### wal-stats
- Shows the state of WAL journals under the workspace, read only

```bash
atomwrite --workspace "$WS" wal-stats
```

- Expect `type` `wal_stats` with `total_journals`, `by_state`, `oldest_journal_age_secs` and `auto_heal_recommended`

### wal-heal
- Removes terminal journals older than a threshold, and keeps journals that are still `Started`
- It needs a path, and `--threshold-secs` defaults to 3600

```bash
atomwrite --workspace "$WS" wal-heal --threshold-secs 3600 .
atomwrite --workspace "$WS" wal-heal --max-duration-ms 200 .
```

- Expect `type` `wal_heal` with `removed`, `preserved`, `malformed` and `bytes_reclaimed`
- `--max-duration-ms` bounds the wall-clock budget, and `--dry-run` previews
- A crash (`kill -9`) can leave a journal in the `Started` state, and `wal-heal` preserves it for you to inspect
- The global `--no-auto-heal` skips the startup heal pass, which runs only on commands that write
- A cancelled run (SIGTERM, SIGINT or a deadline) leaves no journal behind


## Orchestration Commands

### recipe
- Lists and runs built-in multi-step recipes: `search-replace-verify` and `edit-loop-syntax-check`
- `recipe run` needs `--path` or `--target`, and recipe steps skip `*.bak.*` files

```bash
atomwrite --workspace "$WS" recipe list
atomwrite --workspace "$WS" recipe run --name search-replace-verify --pattern beta --replacement omega --path src --dry-run
```

- Expect `recipe` events for the list and a final `recipe_result` with `steps`, `dry_run` and `ok`
- A recipe whose search finds nothing stops at the replace step with exit 1 and `failed_step_id`
- `edit-loop-syntax-check` needs `--pairs-file FILE`, `--target FILE` and `--syntax-check LANGUAGE`
- `--fuzzy`, `--fuzzy-threshold`, `--include` and `--exclude` tune the search-replace recipe

### sparse
- Lists, reads and outlines large trees under a file and byte budget

```bash
printf 'src/lib.rs\nsrc/b.rs\n' > tmp/paths.txt
atomwrite --workspace "$WS" sparse list src/ --max-files 10 --max-bytes 1048576
atomwrite --workspace "$WS" sparse read --paths-file tmp/paths.txt --head 2
atomwrite --workspace "$WS" sparse outline --max-files 5 src/
```

- Expect `sparse_entry`, `sparse_read` or `outline_item` events and a `sparse_summary` with `truncated`
- `sparse list` also takes `--include` and `--exclude` globs, and an invalid glob exits 65
- `sparse outline` reports `sparse_outline_budget`, and a slim build without `ast` exits 78

### semantic-merge
- Merges three files line by line (base, ours, theirs), and it is not AST-aware
- Conflict markers are written by default, and `--no-conflict-markers` turns them off

```bash
printf 'a\nb\nc\n' > tmp/base.txt
printf 'a\nB\nc\n' > tmp/ours.txt
printf 'a\nb\nC\n' > tmp/theirs.txt
atomwrite --workspace "$WS" semantic-merge --base tmp/base.txt --ours tmp/ours.txt --theirs tmp/theirs.txt --output tmp/merged.txt --fail-on-conflict
```

- Expect `type` `semantic_merge` with `status` `merged` and the three input checksums
- A real conflict has `status` `conflict`, and `--fail-on-conflict` turns it into exit 84 (`MERGE_CONFLICT`)
- `--expect-checksum` and the backup flags guard the output file

### semantic-search
- Ranks files and lines by offline Jaccard similarity to a query, and it is always compiled in

```bash
atomwrite --workspace "$WS" semantic-search "unwrap value" src/ --k 5 --min-score 0.01
atomwrite --workspace "$WS" semantic-search "fn" src/ --k 2 --index-dir .atomwrite/semantic-index
```

- Expect `semantic_match` events with `rank`, `score`, `path`, `line` and `snippet`, then a `semantic_summary`
- `backend` is `jaccard`, or `inverted-index` when `--index-dir` is used
- `--include-backups` also scans `.bak` files, and zero hits is not a crash

### codemod
- Applies a YAML list of AST rewrite rules across a tree
- Each rule is an item with `id`, `language`, `pattern` and `rewrite`

```bash
printf -- '- id: unwrap-to-try\n  language: rust\n  pattern: "$E.unwrap()"\n  rewrite: "$E?"\n' > rules.yaml
atomwrite --workspace "$WS" codemod --rules rules.yaml --dry-run src/
```

- Expect `transformed` events and a `codemod_summary` with `by_rule_id`
- A rules file in ast-grep style with `rule:` and `fix:` exits 65
- `transform --rules` and `--inline-rules` read the same format

### watch
- Streams filesystem events with debounce and always ends with a `watch_summary`
- It needs a build with `--features watch`, and the default build exits 78 with a rebuild hint

```bash
atomwrite --workspace "$WS" watch src/ --max-events 1 --idle-exit-ms 500
```

- On the default build, expect `code` `CONFIG_INVALID` and exit 78
- On a `watch` build, expect `watch_summary` as the last line, even on a signal
- Always pass `--max-events` or `--idle-exit-ms` (default idle exit is 500 ms), or `--timeout-secs`
- `--debounce-ms MS`, `--checksum` and `--gitignore true|false` tune the stream

### stat
- Returns metadata only, as an alias of `read --stat`

```bash
atomwrite --workspace "$WS" stat src/lib.rs
```

- Expect `type` `read` with `mode` `stat`, plus `lines`, `bytes` and `checksum`


## Discovery and Host Commands
- Use these before you invent flag lists by hand

### doctor
- Diagnoses the host and reports checks for heal, concurrency, features and the environment
- It reads no environment variable, and `--strict` exits 1 (`DOCTOR_CHECKS_FAILED`) when a check fails

```bash
atomwrite --workspace "$WS" doctor --strict
```

- Expect `type` `doctor_report` with `ok` and a `checks` array
- On the default build a check reports that `watch` is disabled

### commands
- Prints the full command tree as JSON, with or without the global arguments

```bash
atomwrite --workspace "$WS" commands --include-globals
```

- Expect `type` `commands_tree` with 47 `commands` and, with the flag, 33 `global_args`
- Aliases such as `--max-concurrency`, `--timeout`, `--fields` and the hidden `--json` are not listed

### agent-surface
- Prints the tool manifest for agents, and it is the single source of truth for the subcommands

```bash
atomwrite --workspace "$WS" agent-surface --format json
```

- Expect `type` `agent_surface` with `tools`, `exit_codes`, `discovery`, `flag_contract` and `notes`
- `tools` has 47 entries, and `exit_codes` has 41 entries with 33 distinct numbers
- `discovery` lists the `--print-schema` names, and `watch` reports `available: false` on the default build

### locale
- Shows, persists or clears the display language, and it is distinct from the global `--locale`
- Precedence is `--locale`, then the saved preference, then the system language, then `en`

```bash
atomwrite locale
atomwrite locale --set pt-BR
atomwrite locale --clear
```

- Expect `type` `locale_report` with `resolved`, `source`, `available` and `preference_path`
- Only `en` and `pt-BR` exist, and any other tag exits 2 with the list of available tags
- The preference lives in `$XDG_CONFIG_HOME/atomwrite/locale`
- Error `code` and `message` stay in English, and `suggestion` follows the locale, with `suggestion_lang` saying which

### completions
- Prints a shell completion script, or installs it under the XDG data directory
- Supported shells are bash, zsh, fish, elvish and powershell

```bash
atomwrite completions bash > atomwrite.bash
atomwrite completions bash --install
```

- Expect the raw script on stdout for the first call and an `installed` event for the second
- That event is `{"type":"installed","path":"<completion file>","ok":true,"target_resolved":"<same path>","target_source":"argv"}`
- The path comes from the XDG data directory of the shell, as listed in Where Files Live
- A write that fails because the reader closed exits 141 (`BROKEN_PIPE`) with no panic and no crash report
- Very short output, such as `--version | head -1`, can finish before the reader closes and then exits 0

### scratch
- Creates the staging directory under the workspace jail, and never under the OS temporary directory
- `scratch init` needs a path

```bash
atomwrite --workspace "$WS" scratch init .atomwrite/scratch
```

- Expect `type` `scratch_init` with `path`, `relative` and `already_existed`
- Stage `--old-file` and `--new-file` content there, because paths outside the jail exit 126

### config
- Reads and writes persistent tuning keys in the global `config.toml` or the workspace `.atomwrite.toml`
- `list` and `get` need no scope, and `set`, `unset` and `path` need `--scope global|workspace`

```bash
atomwrite --workspace "$WS" config get fuzzy.threshold
atomwrite --workspace "$WS" config list
atomwrite --workspace "$WS" config set fuzzy.threshold 0.9 --scope workspace
atomwrite --workspace "$WS" config path --scope workspace
atomwrite --workspace "$WS" config unset fuzzy.threshold --scope workspace
```

- Expect `type` `config` with `action`, `key`, `value`, `origin` and `default`
- `origin` is `default`, `global`, `workspace`, or `explicit` when `--config PATH` is used
- The 37 keys, their ranges and scopes are in the Configuration section


## 47 Subcommands (SSOT)
- Pin agents to `^0.1.37`, 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 with backup, 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

```bash
atomwrite --workspace "$WS" --sort-by path-line --select type,path,line_number --filter type=match search 'fn' src/
atomwrite --workspace "$WS" --aggregate kind:count list --depth 1 src/
atomwrite --workspace "$WS" --max-output-bytes 12 --allow-truncate --select path,checksum hash src/lib.rs
atomwrite --workspace "$WS" --select ok,error,code,match_count,suggestion --filter ok=false --limit 5 edit src/main.rs --old nothere --new x --fuzzy off
atomwrite --print-schema search
atomwrite --workspace "$WS" list --depth 1 src/ > tmp/base.ndjson
atomwrite --workspace "$WS" --diff-against tmp/base.ndjson list --depth 1 src/
printf '%s\n' '{"path":"src/lib.rs","owner":"alice"}' | atomwrite --workspace "$WS" --join path:owners --select type,path,owners list --depth 1 src/
```

- 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 41 rows and 33 distinct exit numbers, and it comes from `atomwrite agent-surface`
- `atomwrite --help` prints the same list under `Exit codes`
- The error enum has 34 variants, 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 |
| 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 |
| 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 |
| 127 | `SYMLINK_BLOCKED` | 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 `error_class`, `retryable` and `suggestion` live inside it
- Error envelopes carry `error`, `ok`, `code`, `exit`, `message`, `path`, `error_class`, `retryable` and often `suggestion`
- `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

```json
{"error":true,"ok":false,"code":"MATCH_FAILED","exit":65,"message":"match failed: old string not found (fuzzy mode off = exact-only): \"nothere\"","error_class":"permanent","retryable":false,"suggestion":"inspect best_candidate in the error NDJSON and adjust --old, or lower --fuzzy-threshold","match_count":0,"suggestion_lang":"en"}
```

- 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 `origin` `explicit`
- 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 37 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 | true | `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 |
| `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 |
| `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 |
| `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`
- `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`

```bash
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 `origin` `workspace`
- 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 |
| `write.shrink_block_percent` | Shrink guard threshold, default 50 |
| `write.auto_rotate_max_age_secs` | Age window of `--auto-rotate`, default 24 hours |
| `write.content_risk_patterns` | Extra patterns for the write risk assessment |
| `watch.idle_exit_ms`, `watch.debounce_ms` | Defaults of `watch` |
| `search.context`, `search.smart_case`, `search.max_filesize`, `search.max_columns` | Defaults of `search` |
| `defaults.line_ending`, `defaults.max_filesize` | Read by the loader but not used by any command |

```toml
[defaults]
backup = true
retention = 5

[fuzzy]
mode = "auto"
threshold = 0.85

[search]
max_filesize = 10485760
```

- This is a valid `.atomwrite.toml` for the workspace root, and an unknown field such as `defaults.workspace` would exit 78
- `[log]` in a workspace file is ignored, because tracing starts before the workspace file is read

### Where Files Live
- Linux paths were measured, and macOS and Windows paths come from the code (`directories::ProjectDirs`) and were not validated

| Item | Linux path |
|---|---|
| Global config | `$XDG_CONFIG_HOME/atomwrite/config.toml` (default `~/.config/atomwrite/config.toml`) |
| Workspace config | `.atomwrite.toml` in the workspace root |
| Locale preference | `$XDG_CONFIG_HOME/atomwrite/locale` |
| Diagnostics log | `$XDG_STATE_HOME/atomwrite/logs/atomwrite.<date>.jsonl`, file mode 0600 in a 0700 folder |
| Tree-sitter parsers | `$XDG_CACHE_HOME/tree-sitter-language-pack/v1.22.1/` |
| Bash completions | `$XDG_DATA_HOME/bash-completion/completions/atomwrite` |
| Zsh completions | `$XDG_DATA_HOME/zsh/site-functions/_atomwrite` |
| Fish completions | `$XDG_DATA_HOME/fish/vendor_completions.d/atomwrite.fish` |
| Elvish and PowerShell | `$XDG_DATA_HOME/elvish/lib/atomwrite.elv` and `$XDG_DATA_HOME/powershell/Completions/_atomwrite.ps1` |

- The diagnostics log is off by default, so no file is opened and no writer thread starts
- When on, it rotates daily or hourly, keeps 7 files and writes JSON lines
- Stderr diagnostics stay JSON lines either way


## Advanced Patterns

### Optimistic Locking With a Checksum
- Capture the checksum, then write only if the file did not change

```bash
CS="$(atomwrite --workspace "$WS" read src/lib.rs | jaq -r .checksum)"
printf 'pub fn add(a: i32, b: i32) -> i32 {\n    a + b\n}\n\npub fn sub(a: i32, b: i32) -> i32 {\n    a - b\n}\n' | atomwrite --workspace "$WS" write --expect-checksum "$CS" src/lib.rs
```

- Expect `type` `write` with `checksum_before` equal to `$CS`
- A changed file exits 82 (`STATE_DRIFT`), and you reload the checksum and decide again

### Sequential Edits on One File
- Re-capture the checksum before each guarded edit, or accept drift on purpose

```bash
CS="$(atomwrite --workspace "$WS" hash src/b.rs | jaq -r .checksum)"
atomwrite --workspace "$WS" edit --expect-checksum "$CS" src/b.rs --old 'beta' --new 'beta_one'
atomwrite --workspace "$WS" edit --expect-checksum "$CS" --allow-sequential-drift src/b.rs --old 'beta_one' --new 'beta_two'
```

- The first edit passes, and the second would exit 82 without `--allow-sequential-drift`
- Drift accepted that way is logged as a warning on stderr

### Scratch Staging for Multi-line Patches
- Text with quotes, `$` or slashes is fragile in argv, so stage it in files inside the jail

```bash
atomwrite --workspace "$WS" scratch init .atomwrite/scratch
SCR=".atomwrite/scratch"
OLD=$'fn load() -> Option<i32> {\n    Some(1)\n}'
NEW=$'fn load() -> Option<i32> {\n    Some(2)\n}'
printf %s "$OLD" > "$SCR/old.txt"
printf %s "$NEW" > "$SCR/new.txt"
atomwrite --workspace "$WS" edit src/main.rs --old-file "$SCR/old.txt" --new-file "$SCR/new.txt"
```

- Expect `type` `edit` with `edits` 1
- A content file under the OS temporary directory exits 126 (`WORKSPACE_JAIL`)

### Find, Preview, Replace, Confirm
- Locate first, preview the change, apply it, then confirm nothing is left

```bash
atomwrite --workspace "$WS" search 'beta' src/
atomwrite --workspace "$WS" replace --dry-run beta_two beta src/
atomwrite --workspace "$WS" replace beta_two beta src/b.rs
atomwrite --workspace "$WS" search 'beta_two' src/b.rs
```

- The last search exits 1, which means no match is left in the file
- A successful `replace` deletes the `.bak` copy it made, and `search` still walks `.bak` files, so name the file

### Atomic Multi-file Change
- A batch with `--transaction` commits all operations or none

```bash
printf '%s\n' '{"op":"write","target":"b1.txt","content":"x"}' '{"op":"copy","source":"b1.txt","target":"b2.txt"}' '{"op":"move","source":"b2.txt","target":"b3.txt"}' '{"op":"delete","target":"b3.txt"}' > tmp/manifest.ndjson
atomwrite --workspace "$WS" batch --file tmp/manifest.ndjson --transaction
```

- Expect four `batch_op` lines and a `summary` with `committed` set to `true`

### Undo a Bad Edit
- Take a backup before a risky change and restore it if the result is wrong

```bash
atomwrite --workspace "$WS" backup src/lib.rs
printf 'broken\n' | atomwrite --workspace "$WS" write --allow-shrink src/lib.rs
atomwrite --workspace "$WS" rollback src/lib.rs --latest --verify
```

- Expect `type` `rollback` with `verified` set to `true`
- The file is back to its content before the bad write

### Compact Output for Agents
- Ask for only the keys you need, and filter on the outcome

```bash
atomwrite --workspace "$WS" --select ok,code,match_count,suggestion --filter ok=false --limit 5 edit src/main.rs --old nothere --new x --fuzzy off
```

- Expect one line such as `{"ok":false,"code":"MATCH_FAILED","match_count":0,"suggestion":"..."}` and exit 65
- `--filter` never hides an error envelope, so a failure with `ok: false` always arrives


## Integration With Shell Scripts
- Check the exit code first, and treat exit 1 from search-like commands as an answer
- Use `jaq` for fields, and keep `${PIPESTATUS[0]}` when a pipe follows `atomwrite`

```bash
if atomwrite --workspace "$WS" search 'TODO' src/ > tmp/hits.ndjson; then
  echo "found"
else
  code=$?
  if [ "$code" -eq 1 ]; then echo "none"; else echo "failed: $code"; fi
fi
```

- Expect `found` while a `TODO` exists, and `none` once it is gone
- A write whose pipe feeds `jaq` hides its status unless you read `${PIPESTATUS[0]}`

```bash
out="$(atomwrite --workspace "$WS" edit src/main.rs --old nothere --new x --fuzzy off)"; code=$?
printf '%s\n' "$out" | jaq -r .ok
echo "$code"
```

- Expect `false` and `65`
- 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]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

```rust
use std::process::Command;

fn main() -> 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`

```json
{"error":true,"ok":false,"code":"WORKSPACE_JAIL","exit":126,"message":"path outside workspace jail: /etc/passwd (workspace: /home/user/project)","path":"/etc/passwd","error_class":"precondition_failed","retryable":false,"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)","workspace":"/home/user/project","suggestion_lang":"en"}
```

### 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]INSTALL.md for the full Windows guide and troubleshooting


## Release History
- This section is historical, and the details live in the [CHANGELOG]../CHANGELOG.md and [MIGRATION]MIGRATION.md

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


## Next Steps
- Browse longer recipes in the [COOKBOOK]COOKBOOK.md
- Read the agent contract in [AGENTS.md]AGENTS.md
- Check platform notes in [CROSS_PLATFORM.md]CROSS_PLATFORM.md
- Upgrade with [MIGRATION.md]MIGRATION.md, and install with [INSTALL.md]INSTALL.md
- Validate output with the schemas in [docs/schemas]schemas/README.md