patch-cc 0.5.2__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {patch_cc-0.5.2 → patch_cc-0.6.1}/PKG-INFO +13 -12
  2. {patch_cc-0.5.2 → patch_cc-0.6.1}/README.md +12 -11
  3. {patch_cc-0.5.2 → patch_cc-0.6.1}/docs/CONDUCT.md +14 -2
  4. patch_cc-0.6.1/docs/INTERNALS.md +273 -0
  5. {patch_cc-0.5.2 → patch_cc-0.6.1}/docs/PLAYBOOK.md +43 -14
  6. {patch_cc-0.5.2 → patch_cc-0.6.1}/docs/corpus.md +72 -29
  7. patch_cc-0.6.1/scripts/corpus.py +370 -0
  8. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/__init__.py +1 -1
  9. patch_cc-0.6.1/src/patch_cc/bun/blob.py +286 -0
  10. patch_cc-0.6.1/src/patch_cc/bun/container.py +204 -0
  11. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/bun/elf.py +5 -10
  12. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/bun/macho.py +4 -9
  13. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/cli.py +3 -11
  14. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/doctor.py +3 -4
  15. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/menu.py +2 -2
  16. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patcher.py +9 -2
  17. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/streaming.py +36 -82
  18. patch_cc-0.5.2/docs/INTERNALS.md +0 -266
  19. patch_cc-0.5.2/src/patch_cc/bun/blob.py +0 -544
  20. patch_cc-0.5.2/src/patch_cc/bun/container.py +0 -276
  21. {patch_cc-0.5.2 → patch_cc-0.6.1}/.github/workflows/ci.yml +0 -0
  22. {patch_cc-0.5.2 → patch_cc-0.6.1}/.github/workflows/release-watch.yml +0 -0
  23. {patch_cc-0.5.2 → patch_cc-0.6.1}/.github/workflows/release.yml +0 -0
  24. {patch_cc-0.5.2 → patch_cc-0.6.1}/.gitignore +0 -0
  25. {patch_cc-0.5.2 → patch_cc-0.6.1}/AGENTS.md +0 -0
  26. {patch_cc-0.5.2 → patch_cc-0.6.1}/CLAUDE.md +0 -0
  27. {patch_cc-0.5.2 → patch_cc-0.6.1}/LICENSE +0 -0
  28. {patch_cc-0.5.2 → patch_cc-0.6.1}/docs/demo.gif +0 -0
  29. {patch_cc-0.5.2 → patch_cc-0.6.1}/pyproject.toml +0 -0
  30. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/bun/__init__.py +0 -0
  31. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/bun/errors.py +0 -0
  32. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/cache.py +0 -0
  33. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/codex/__init__.py +0 -0
  34. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/codex/gateway.py +0 -0
  35. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/codex/models.py +0 -0
  36. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/codex/oauth.py +0 -0
  37. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/codex/translate.py +0 -0
  38. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/js.py +0 -0
  39. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/locate.py +0 -0
  40. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/__init__.py +0 -0
  41. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/agents.py +0 -0
  42. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/base.py +0 -0
  43. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/chrome.py +0 -0
  44. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/codex.py +0 -0
  45. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/output.py +0 -0
  46. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/patches/thinking.py +0 -0
  47. {patch_cc-0.5.2 → patch_cc-0.6.1}/src/patch_cc/ui.py +0 -0
  48. {patch_cc-0.5.2 → patch_cc-0.6.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patch-cc
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: Interactive patcher for the Claude Code native binary
5
5
  Project-URL: Homepage, https://github.com/anfreire/patch-cc
6
6
  Project-URL: Repository, https://github.com/anfreire/patch-cc
@@ -45,7 +45,7 @@ No Node, no Bun.
45
45
  uvx patch-cc # fullscreen menu, no install needed
46
46
  ```
47
47
 
48
- ![patch-cc: pick tweaks, register a Codex model, pin the Plan agent to it, bake — the patched binary is 161 MB smaller](https://raw.githubusercontent.com/anfreire/patch-cc/main/docs/demo.gif)
48
+ ![patch-cc: pick tweaks, register a Codex model, pin the Plan agent to it, bake](https://raw.githubusercontent.com/anfreire/patch-cc/main/docs/demo.gif)
49
49
 
50
50
  ## Requirements
51
51
 
@@ -195,19 +195,20 @@ uvx patch-cc apply --brand --model Explore=haiku
195
195
  `uvx patch-cc status` tells you whether the current binary is patched, and the
196
196
  startup name / `--version` marker are visible tells too.
197
197
 
198
- ## Why native-only, and why it stays small
198
+ ## Why native-only, and what the write does
199
199
 
200
200
  Claude Code now ships only as a Bun single-file executable; the npm package is a
201
201
  wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
202
- binary's `.bun` section in place — since 2.1.242 the app is code-split across
203
- ~1,300 of them, and patch-cc treats every one as a single surface. It also drops
204
- the stale precompiled bytecode of the modules it edits editing a module's
205
- source invalidates its bytecode anyway so on Linux, where the ELF section is
206
- rewritten in place, a patched binary is *smaller* than the original, not larger
207
- (83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
208
- the file keeps its size; it still runs correctly.)
209
- [docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
210
- measurements; `patch-cc status` has yours.
202
+ binary's `.bun` section — since 2.1.242 the app is code-split across ~1,300 of
203
+ them, and patch-cc treats every one as a single surface. The write never moves a
204
+ byte of the original: each edited module's source is appended and its stale
205
+ precompiled bytecode is unlinked (editing a module's source invalidates its
206
+ bytecode anyway), so a patched binary is a few percent larger than the original,
207
+ and everything else the binary carries Bun's own records, however its format
208
+ grows is exactly where it was. That is what keeps the container layer out of
209
+ the way when Bun changes its format under Claude.
210
+ [docs/INTERNALS.md](docs/INTERNALS.md#the-rule-never-move-a-pristine-byte) has
211
+ the reasoning; `patch-cc status` has your numbers.
211
212
 
212
213
  See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
213
214
  [docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
@@ -14,7 +14,7 @@ No Node, no Bun.
14
14
  uvx patch-cc # fullscreen menu, no install needed
15
15
  ```
16
16
 
17
- ![patch-cc: pick tweaks, register a Codex model, pin the Plan agent to it, bake — the patched binary is 161 MB smaller](https://raw.githubusercontent.com/anfreire/patch-cc/main/docs/demo.gif)
17
+ ![patch-cc: pick tweaks, register a Codex model, pin the Plan agent to it, bake](https://raw.githubusercontent.com/anfreire/patch-cc/main/docs/demo.gif)
18
18
 
19
19
  ## Requirements
20
20
 
@@ -164,19 +164,20 @@ uvx patch-cc apply --brand --model Explore=haiku
164
164
  `uvx patch-cc status` tells you whether the current binary is patched, and the
165
165
  startup name / `--version` marker are visible tells too.
166
166
 
167
- ## Why native-only, and why it stays small
167
+ ## Why native-only, and what the write does
168
168
 
169
169
  Claude Code now ships only as a Bun single-file executable; the npm package is a
170
170
  wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
171
- binary's `.bun` section in place — since 2.1.242 the app is code-split across
172
- ~1,300 of them, and patch-cc treats every one as a single surface. It also drops
173
- the stale precompiled bytecode of the modules it edits editing a module's
174
- source invalidates its bytecode anyway so on Linux, where the ELF section is
175
- rewritten in place, a patched binary is *smaller* than the original, not larger
176
- (83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
177
- the file keeps its size; it still runs correctly.)
178
- [docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
179
- measurements; `patch-cc status` has yours.
171
+ binary's `.bun` section — since 2.1.242 the app is code-split across ~1,300 of
172
+ them, and patch-cc treats every one as a single surface. The write never moves a
173
+ byte of the original: each edited module's source is appended and its stale
174
+ precompiled bytecode is unlinked (editing a module's source invalidates its
175
+ bytecode anyway), so a patched binary is a few percent larger than the original,
176
+ and everything else the binary carries Bun's own records, however its format
177
+ grows is exactly where it was. That is what keeps the container layer out of
178
+ the way when Bun changes its format under Claude.
179
+ [docs/INTERNALS.md](docs/INTERNALS.md#the-rule-never-move-a-pristine-byte) has
180
+ the reasoning; `patch-cc status` has your numbers.
180
181
 
181
182
  See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
182
183
  [docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
@@ -74,6 +74,18 @@ shape that already absorbs every case, found rather than bolted on.
74
74
  React idioms before it was cut to three insertions on the tool-use list
75
75
  ([PLAYBOOK.md](PLAYBOOK.md#live-thinking--streamingpy)).
76
76
 
77
+ - **Never move a byte you did not write.** The container is the second worked
78
+ case of the rule above. Bun reads a payload through its pointer and ignores
79
+ what nothing points at; a rewrite that re-lays the arena has to re-aim every
80
+ pointer in the blob, including ones in records it has never seen, and Bun
81
+ adds such records whenever it likes — 2.1.246 corrupted one, 2.1.269 was
82
+ refused over one, and the whitelist of known records between the two could
83
+ never be finished. So the write appends what changed and copies everything
84
+ else where it was, and needs to know only what it touches
85
+ ([INTERNALS.md](INTERNALS.md#the-rule-never-move-a-pristine-byte)). The price
86
+ is a binary a few percent larger instead of smaller; the size win was the
87
+ claim that bought the fragility.
88
+
77
89
  - **Port faithfully.** When you change a patch, verify its output against a real
78
90
  bundle — byte-identical where behaviour must not change. `doctor` over the
79
91
  archived corpus is that check, and the *diff* between two sweeps is the half
@@ -84,5 +96,5 @@ shape that already absorbs every case, found rather than bolted on.
84
96
  - **The user controls commits and releases.** Don't commit, push, or publish
85
97
  unless asked.
86
98
 
87
- The binary format, and why the ELF write is in-place and the bytecode is
88
- dropped: [INTERNALS.md](INTERNALS.md).
99
+ The binary format, and why the write appends rather than compacts:
100
+ [INTERNALS.md](INTERNALS.md).
@@ -0,0 +1,273 @@
1
+ # Internals
2
+
3
+ How patch-cc gets from a Claude binary to a patched one.
4
+
5
+ ## The shape of a native Claude binary
6
+
7
+ Claude Code ships as a [Bun](https://bun.sh) single-file executable. The whole
8
+ app — 34 MB of minified JS on 2.1.269, plus a few asset modules — is embedded
9
+ in the binary:
10
+
11
+ - **Linux**: an ELF section named `.bun`
12
+ - **macOS**: a Mach-O section `__BUN,__bun`
13
+ - **Windows**: a PE `.bun` section (not supported here)
14
+
15
+ Inside that section is a *Bun module graph*: a flat arena of payloads, a module
16
+ table describing them, and a trailer. Before 2.1.242 the graph was the app in one
17
+ module plus a few asset modules; since then the app is **code-split** across
18
+ ~1,300 `chunk-*.js` modules that the entry lazily imports (see
19
+ [the split](#the-21242-split-and-the-patchable-surface)). Either way patch-cc
20
+ treats every module the container declares to be JavaScript as one surface.
21
+
22
+ ```
23
+ .bun section
24
+ └── [u64 size prefix]
25
+ └── Bun blob
26
+ ├── payload arena name / contents / sourcemap / bytecode / ... bytes,
27
+ │ and whatever else the builder placed there
28
+ ├── module table N records × 52 bytes
29
+ ├── records flag-gated, see below
30
+ ├── compileExecArgv
31
+ ├── offsets struct 32 bytes: byteCount, modulesPtr, entryId, argvPtr, flags
32
+ └── "\n---- Bun! ----\n" 15-byte trailer
33
+ ```
34
+
35
+ Every pointer is a `(u32 offset, u32 length)` pair relative to the blob start,
36
+ and Bun reads a payload *through* its pointer — bytes nothing points at are
37
+ ignored. A module record is six such pairs — `name`, `contents`, `sourcemap`,
38
+ `bytecode`, `moduleInfo`, `bytecodeOriginPath` — followed by four `u8` flags
39
+ (`encoding`, `loader`, `moduleFormat`, `side`).
40
+
41
+ Code: `src/patch_cc/bun/blob.py`.
42
+
43
+ ## The rule: never move a pristine byte
44
+
45
+ The loader's contract above is the only thing the rewrite rides. How the
46
+ builder lays the arena out, and which records follow the table, is the half of
47
+ the format that changes with Bun — and a rewrite that re-laid the arena would
48
+ have to find and re-aim every pointer in the blob, including pointers in records
49
+ it had never seen. Bun owes that to nobody, and it is where this tool broke
50
+ twice: 2.1.246 (Bun 1.4.1) chained records after the table, and a chain-blind
51
+ compaction shipped a binary that segfaulted in Bun's graph loader while every
52
+ module compared equal; 2.1.269 (Bun 1.4.3) chained two more, and the chain
53
+ walker written after 2.1.246 — mirroring Bun's reader record for record, with a
54
+ whitelist of the bits it knew — refused the build outright. Mirroring the
55
+ reader more completely is a repair that can never be finished.
56
+
57
+ So `rewrite` moves nothing. An edited module's text is appended after the
58
+ arena (NUL-terminated, as Bun's own `count_z` payloads are); the module table
59
+ and everything after it are copied verbatim, shifted by a multiple of 128 so
60
+ every phase inside them holds; then the pointers that name what changed are
61
+ re-aimed:
62
+
63
+ - the edited module's `contents` pair, at its text;
64
+ - its `bytecode` pair, at nothing ([below](#the-bytecode-and-why-we-unlink-it));
65
+ - its source-hash word, to 0 — upstream's own "none, compute it" — because the
66
+ hash keys the text in JSC and the pristine text's hash must not describe ours;
67
+ - the offsets struct's `modulesPtr` and `argvPtr`, along with the bytes they
68
+ name.
69
+
70
+ Every other byte, and every other pointer, is exactly where it was. A record
71
+ patch-cc has never heard of comes through the same copy and still points where
72
+ it did, because what it points at did not move; the arena's payloads keep their
73
+ 128-byte phase — Bun deserializes bytecode in place and calls misalignment "a
74
+ runtime assertion error or segfault" — because the arena is not shifted at all.
75
+
76
+ What has to be *known* is exactly what is touched: the offsets struct, two
77
+ pairs and the loader byte of a module record, and the one record that
78
+ describes a module's text — its hash, first after the table when bit 5 of
79
+ `flags` says there is one. The rest of `flags` travels verbatim, bits this code
80
+ has never seen included, with one exception. Bit 4 declares every module's
81
+ text to lie in one contiguous run: Bun's runtime hints the kernel to drop those
82
+ pages after startup, and its comment forbids any other region inside the run.
83
+ The appended text is outside it, so the bit is cleared — Bun documents the
84
+ absent flag as an older layout it reads — rather than left asserting something
85
+ false.
86
+
87
+ ## The records after the table
88
+
89
+ Bun ≥ 1.4.1 chains optional records directly after the module table, each
90
+ announced by a `flags` bit and read back in flag order. What has been seen so
91
+ far, for orientation — patch-cc parses none of it beyond the first:
92
+
93
+ | bit | record | since |
94
+ |---|---|---|
95
+ | 5 | `[u32; modules]` — each module's WTF hash of its source text (0 = none) | 2.1.246 |
96
+ | 6 | `u32 count`, then `count` × `{u32 id, ptr}` — internal-module bytecode | 2.1.246 |
97
+ | 7 | one pointer: the shared bytecode string table every chunk's bytecode indexes | 2.1.246 |
98
+ | 8 | `u32` — how many leading modules load before the first `import()` | 2.1.248 |
99
+ | 9 | one pointer: the string table `moduleInfo` bodies index | 2.1.248 |
100
+ | 11 | one pointer to a pre-linked ES module graph, then `u32 count` and `count` × `u32` file index | 2.1.269 |
101
+ | 12 | `u32 flags, u32 value` — runtime options (a JIT policy) | 2.1.269 |
102
+
103
+ Three shapes across the 17 published builds from 2.1.246 to 2.1.269, and none
104
+ of them a code change here: each is bytes in the tail and payloads in the
105
+ arena, and both are copied where they are. The one record whose *meaning* the
106
+ rewrite leans on is bit 11's: a module in the pre-linked graph ships no
107
+ `moduleInfo` body, and its imports and exports are read from the graph rather
108
+ than its text — which holds for an edited module too, because a patch never
109
+ touches module linkage (the syntax gate reads past it,
110
+ [PLAYBOOK.md](PLAYBOOK.md#the-many-module-surface)).
111
+
112
+ ## The 2.1.242 split, and the patchable surface
113
+
114
+ Through 2.1.241 the entrypoint module *was* the app: one ~28 MB `contents`
115
+ carrying every line patch-cc anchors on. 2.1.242 turned on Bun code-splitting
116
+ with lazy loading, and the shape changed under the tool:
117
+
118
+ | build | modules | entrypoint `contents` |
119
+ |---|---|---|
120
+ | 2.1.241 | 11 | 28,249,679 bytes (the whole app) |
121
+ | 2.1.243 | 1,385 | 19,952 bytes (an argv shim) |
122
+
123
+ The entrypoint is now a ~20 KB shim that parses argv and lazily
124
+ `import()`s the app across ~1,300 `/$bunfs/root/chunk-*.js` modules; ~46 MB of
125
+ JS, the largest chunk 7.3 MB. The code did not disappear — every anchor is still
126
+ in the binary — but it left the one module patch-cc used to read, and it does not
127
+ concentrate in a single chunk (`branding` spans a dozen modules, `org-label` ten,
128
+ `spinner-tips` six).
129
+
130
+ So the patchable surface is **every module the container declares to be
131
+ JavaScript**, discovered the way the entrypoint itself is discovered — off the
132
+ artifact, never hardcoded. A module's *loader* (the second trailing flag) is how
133
+ Bun decides whether to compile it as source or hand it over as opaque bytes, and
134
+ the entrypoint is by definition the JS Bun runs, so its loader *is* the JS loader
135
+ (`Blob.js_modules`). The asset modules (the native addons, the bundled
136
+ `mermaid`/`hljs`, the HTML template) carry other loaders and are left alone. A
137
+ pre-split build is the one-module case of this — `js_modules()` returns just the
138
+ monolith — so [`js.Source`](PLAYBOOK.md#the-many-module-surface) spans one module
139
+ or a thousand through the same code.
140
+
141
+ The entrypoint still matters for one thing: it is where the manifest lives and
142
+ what `status` reads, named by the offsets struct's `entry_point_id` — the same
143
+ index Bun resolves it by. Its *name* is upstream's to change and we never read
144
+ it: 2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
145
+
146
+ ## The bytecode, and why we unlink it
147
+
148
+ Modules carry precompiled Bun **bytecode**. Before the split only the entry
149
+ module had any (~half of the binary); the code-split builds carry it on nearly
150
+ every chunk (78 MB of 219 on 2.1.268, across ~1,650 modules), and the records
151
+ after the table name another 14 MB — the internal modules' bytecode and the
152
+ shared string table — that no edit touches.
153
+
154
+ Bun runs a module's bytecode in preference to its source, so an edited module's
155
+ bytecode would run the *unpatched* code. `rewrite` therefore aims the edited
156
+ modules' `bytecode` pair at nothing, and Bun compiles those modules from source
157
+ at launch; every untouched module keeps its bytecode and its fast start. The
158
+ stale bytes themselves stay in the file, unreferenced: reclaiming them would
159
+ mean moving everything after them, which is the compaction the rule above
160
+ forbids. A patched binary is therefore a few percent *larger* than the
161
+ original — the edited modules' text, appended — never smaller.
162
+
163
+ Measured on 2.1.268, the default patch set:
164
+
165
+ | binary | size | bytecode the table names |
166
+ |---|---|---|
167
+ | pristine | 219 MB | 78 MB, every module |
168
+ | patched (11 modules edited) | 228 MB | 51 MB, the untouched modules |
169
+
170
+ Read the current figures off any binary with `patch-cc status` rather than off
171
+ this table. Startup does not move: an edited module recompiles from source
172
+ whether its bytecode is dropped or merely unlinked, and the recompile is per
173
+ lazily-imported edited module rather than the whole app at once.
174
+
175
+ Every write asserts each **edited** module names no bytecode in the binary it
176
+ produced (`container.verify`), and `doctor`'s smoke bake writes a temp binary
177
+ through the same `container.write` and *executes* it, so the sweep exercises
178
+ the assert, and the loader itself, on every corpus build. If a future Bun build
179
+ makes bytecode authoritative over source, that assert is the tripwire — every
180
+ edit would silently no-op otherwise.
181
+
182
+ ## Writing it back
183
+
184
+ `.bun` is the last *allocated* ELF section; only non-allocated metadata
185
+ (`.comment`, `.symtab`, `.strtab`, `.shstrtab`) follows it. patch-cc rewrites
186
+ the ELF bytes in place:
187
+
188
+ 1. Splice the new, larger blob over the old `.bun` bytes.
189
+ 2. Shift `e_shoff`, `e_phoff`, and the trailing non-alloc sections/segments by
190
+ the size delta, rounded up to the strictest alignment among them.
191
+ 3. Grow the containing `PT_LOAD` segment's `filesz`/`memsz` to match.
192
+
193
+ `.bun` keeps its original file offset. This is deliberately *not* done with a
194
+ general ELF library: LIEF rebuilds the binary and relocates `.bun` so its file
195
+ offset equals its virtual address (`0x20000000`), which inflates the file to
196
+ ~715 MB. Raw in-place surgery avoids that entirely.
197
+
198
+ Guards refuse anything that could corrupt the mapping: allocated sections after
199
+ `.bun`, growth into a header table, an unrelated spanning segment, or a
200
+ misaligned `PT_LOAD` shift. If any fires, the write aborts rather than guesses.
201
+
202
+ Code: `src/patch_cc/bun/elf.py`. macOS uses LIEF (`macho.py`): growing a
203
+ Mach-O segment is page-aligned and bounded, with no relocation pathology, and
204
+ growth is the only thing the rewrite ever asks of a container, so the two
205
+ platforms behave the same way. Every edit is followed by an ad-hoc `codesign`
206
+ (mandatory on Apple Silicon).
207
+
208
+ ## The manifest
209
+
210
+ Every patched bundle carries a single comment line — appended to the **entry
211
+ module**, the one module always present and always re-extracted, and the one
212
+ `status` reads — describing its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it
213
+ means for matcher health:
214
+
215
+ ```
216
+ //patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"suffix":...,
217
+ "models":{...},"org":...,"codex":{"port":8817,"models":["gpt-5.6-sol"]}}
218
+ ```
219
+
220
+ Every key after `patches` is a configurable patch's own, declared in one place
221
+ (`Patch.setting`) so the manifest here, the cache, and the menu cannot spell it
222
+ three ways. Each is written only when *that* patch landed **and** has a value
223
+ worth recording, so `status` can never assert a name, marker, or model the
224
+ bundle does not contain — and so this is the *widest* the line gets, not its
225
+ fixed shape.
226
+
227
+ That line is why `patch-cc status` can name exactly what is applied: several
228
+ patches are value flips (`verbose:!0`) that leave no other trace. A comment
229
+ can't collide with code and travels with the bundle through extract/repack.
230
+ The menu also reads it to pre-select the current patch set — the binary is the
231
+ state.
232
+
233
+ Each key records what was *asked for*, never what was derived from it. `codex`
234
+ carries model ids and a port and nothing else: a Codex model's display name and
235
+ context window are already baked into the bundle, and repeating them here would
236
+ be a second copy — one that a relabelling upstream could make disagree with the
237
+ binary it claims to describe. That is also what makes the manifest the single
238
+ home for the gateway port: `codex serve` and `codex status` read it from here
239
+ rather than from a store of their own.
240
+
241
+ ## Safety
242
+
243
+ - Before the first patch of a version, the pristine binary is copied to
244
+ `~/.local/share/patch-cc/backups/`. `restore` copies it back — never an
245
+ inverse patch (insertions cascade, so a reverse diff is meaningless).
246
+ - Patching always starts from that pristine copy, so re-applying never stacks
247
+ edits on edits, and an apply where **nothing lands** — every selected patch
248
+ broken, so the manifest would claim nothing — leaves the binary untouched
249
+ entirely (unlinking bytecode for nothing would only slow startup). A patch
250
+ that *lands* still writes even where it changed no bytes, because landing
251
+ includes an override the build already satisfies: the manifest records what
252
+ was asked and verified present, so `status` can report it.
253
+ - The bundle is parsed, and any syntax error aborts before the binary or the
254
+ backup is touched — the two checks below answer "did we write what we meant
255
+ to", which a corrupt splice satisfies perfectly. The parse is the same one
256
+ the patches locate with, so it costs nothing extra and sits at *every* batch
257
+ of edits as well as on the final bytes: a patch that produces rubble is named
258
+ and dropped rather than aborting the run. See
259
+ [PLAYBOOK.md](PLAYBOOK.md#the-syntax-gate).
260
+ - Every write is verified: patch-cc re-extracts the JS from the binary it just
261
+ wrote and asserts every module equals what it meant to write, and that the
262
+ blob is the pristine blob plus exactly the intended edits — the arena
263
+ byte-identical in place; the module table and the tail verbatim one shift
264
+ along, except each edited module's two pairs and hash word; the flags less
265
+ the contiguity bit; the same entry module and the same `compileExecArgv`.
266
+ Nothing is enumerated, so a record this code has never parsed is covered by
267
+ the same comparison as the ones it has. The check exists because 2.1.246
268
+ failed *only* there: every module compared equal while the written binary
269
+ was dead.
270
+ - Patching a binary that is already marked, when no pristine backup exists, is
271
+ refused outright — there is nothing clean to start from, and our edits change
272
+ lengths, so a second pass would corrupt rather than update. `restore` or a
273
+ reinstall are the only honest fixes; there is deliberately no override.
@@ -123,6 +123,20 @@ The tree removes the *mechanical* fragility. It does not decide these:
123
123
  the bundle one identity, the point where that path is fed, where a twin of
124
124
  the path owes one per step, each a claim only the patch needs.
125
125
  `live-thinking` is the worked case ([below](#live-thinking--streamingpy)).
126
+ - **Anchor on the fact, not on a consumer of it.** When upstream keeps a fact
127
+ under a name of its own — a setting, a header, a store field — and every
128
+ site that acts on it sits downstream of one read, default or flip *that
129
+ read* and leave the sites alone: the sites are where upstream respells
130
+ things, the name is what it keeps. `display-mode` paid for the lesson on
131
+ 2.1.277. It defaulted the request's thinking display at the site that chose
132
+ it, reached through the env-var read that shared the value's `let`
133
+ (comma-fusion, the minifier's) and identified as the consequence of a
134
+ ternary whose alternative was `void 0` (the branch layout, upstream's); the
135
+ build flipped the branches and grew a `"highlights"` mode inside the value,
136
+ and the step found nothing with every anchor standing — while the fact,
137
+ `showThinkingSummaries`, was one member read in one spelling on every build
138
+ in the corpus. Where a value *must* be identified at its site, ask what it
139
+ can *be* (`js.values`), never which branch it sits on.
126
140
  - **Match an identity by the weakest claim that still proves it; hold a rewrite
127
141
  to the exact node.** The two jobs pull in opposite directions.
128
142
  `thinking-inline`'s null-guard neutralises only a `return` that answers
@@ -215,7 +229,7 @@ Anything the binary can enumerate for us, it does:
215
229
  three entries when 2.1.229 renamed the module out from under it
216
230
  (`…/src/entrypoints/cli.js` → `…/cli`) and the tool stopped opening binaries
217
231
  at all — with the answer already parsed, already carried on the dataclass, and
218
- already written back unchanged by `rebuild`. A list that only ever grows is
232
+ already written back unchanged by `rewrite`. A list that only ever grows is
219
233
  answering the wrong question. An id past the end of the module table is a
220
234
  corrupt blob and raises; there is deliberately no fallback to guessing by
221
235
  name, because a container that cannot say which module it runs is not one to
@@ -505,8 +519,9 @@ Most patches are one rewrite. `live-thinking` is five named sub-steps, because
505
519
  a single hit count cannot tell "all landed" from "half silently drifted", and
506
520
  each records its own `candidates`/`applied`:
507
521
 
508
- - **`display-mode`** — the request asks the API for summarised thinking text;
509
- without it the stream carries signatures and no words.
522
+ - **`display-mode`** — the `showThinkingSummaries` setting defaults on, so the
523
+ request asks the API for summarised thinking text; without it the stream
524
+ carries signatures and no words.
510
525
  - **`reducer`** — the stream reducer is found together with the options bag it
511
526
  is handed the live tool-use setter in, and our binding of that setter is
512
527
  added to the bag.
@@ -602,7 +617,9 @@ that broke a patch along with every note.
602
617
 
603
618
  6. Sweep the fix over the versions you still have. `doctor` takes a path, and
604
619
  every binary patch-cc has ever touched left a pristine copy in
605
- `~/.local/share/patch-cc/backups/`:
620
+ `~/.local/share/patch-cc/backups/` — which is only the builds *this machine*
621
+ patched, so `uv run scripts/corpus.py sync` first to make that the published
622
+ set ([corpus.md](corpus.md)):
606
623
 
607
624
  ```bash
608
625
  sweep() { for b in ~/.local/share/patch-cc/backups/*.orig; do
@@ -879,15 +896,24 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
879
896
  drew beside the landed block for as long as the answer streamed. A
880
897
  tool-use entry at the same index is left exactly as the twin leaves it,
881
898
  and a list with nothing to drop is returned as it was.
882
- - **`display-mode`** defaults the request's thinking display to
883
- `"summarized"`; without it the API only streams summary text when the
884
- `showThinkingSummaries` setting is on. Two shapes used to be spelled out —
885
- the inline env check and the 2.1.216 form that hoists it behind
886
- feature-helper calls. They are one edit: the display value gains a
887
- default, and whatever guards reach it are untouched because they are never
888
- matched. The env-var *name* is the witness; whatever reads it is never
889
- described, so a hoisted `process.env` (the same migration that killed
890
- `org-label` on 2.1.228) costs nothing here.
899
+ - **`display-mode`** defaults the `showThinkingSummaries` setting on. Upstream
900
+ asks the API for `"summarized"` exactly when that setting is on, and routes
901
+ every consequence the request's display, the beta header it withholds
902
+ while summaries are on, the `updates` display the request otherwise falls
903
+ back to off one read of it, so that read
904
+ gains a default (`(settings().showThinkingSummaries??!0)`) and the request
905
+ is never touched. Every member read of the name, as `spinner-tips` reads
906
+ its setting, and a write is skipped for the reason it gives; the read is
907
+ one on every build in the corpus, spelled `settings().name??!1` on all of
908
+ them, and none of that is claimed. An explicit `false` in the user's
909
+ settings still wins. Until 2.1.277 this step defaulted the request's
910
+ display value instead, reached through the `CLAUDE_CODE_DISABLE_THINKING`
911
+ read that shared its `let` and identified as the consequence of a ternary
912
+ whose alternative was `void 0`; that build flipped the branches
913
+ (`!x?void 0:…`) and grew a `"highlights"` mode inside the chosen value,
914
+ and the step read *found nothing* with every anchor standing. Both claims
915
+ were syntax between the anchors ([What still takes
916
+ judgement](#what-still-takes-judgement)).
891
917
 
892
918
  **What is deliberately not done.** No state of ours: upstream's store carries
893
919
  a `streamingThinking` slot with a setter and a thirty-second linger, written
@@ -934,7 +960,10 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
934
960
  slots (2.1.257 — not the compiler arriving, which had cached the
935
961
  conversation render since 2.1.247, but this component joining it). The
936
962
  reducer half, anchored on the API's event strings, has not moved since it
937
- became an insertion at dispatch points. Prefer claims upstream cannot drop
963
+ became an insertion at dispatch points. The display default broke once
964
+ (2.1.277), on the branch layout of the ternary that chose the request's
965
+ value, and moved off the request onto the setting upstream chooses it from.
966
+ Prefer claims upstream cannot drop
938
967
  without paying for them — a dispatch string, a store's field name, the twin's
939
968
  own list — over claims only this patch needs, which is what CONDUCT's *ride
940
969
  what upstream ships working* asks. What was cut, and why each piece was a
@@ -10,26 +10,32 @@ claim is one command from re-verification instead of a number you have to trust.
10
10
  Before the first patch of a version, patch-cc copies the pristine binary to
11
11
  `~/.local/share/patch-cc/backups/<version>.orig` ([INTERNALS.md](INTERNALS.md#safety)).
12
12
  The corpus is exactly those copies: not a fixture checked into the repo (each is
13
- ~300 MB), but a set that **accretes on its own** as Claude auto-updates and you
14
- re-patchand that can be backfilled by saving any pristine native binary under
15
- the same name. `doctor <path>` reads one; the sweep below reads all of them.
13
+ a few hundred MB), but a directory this machine fills. Left alone it fills
14
+ **unevenly**a build patched here leaves its `.orig`, a build that shipped while
15
+ this machine sat idle leaves nothing so the set samples an update cadence rather
16
+ than a release history, and its holes are invisible from inside it.
17
+ `scripts/corpus.py` closes that: Anthropic still serves every build it published,
18
+ so the set is completed off the artifact, like everything else here.
16
19
 
17
20
  The binaries are the artifact, not this file — `doctor` recomputes every count
18
- from them, so nothing here can drift from what a matcher actually does. This
19
- file is a fixed point to check the binaries *against*: a hash that no longer
20
- matches means the file changed under you, not that a number moved.
21
+ from them, so nothing here can drift from what a matcher actually does. Nor is
22
+ this file where "pristine" is defined: every release manifest carries a
23
+ per-platform sha256 and each `.orig` is checked against it. The table below is
24
+ where that check is written down once per build, so its hashes are the offline
25
+ record for builds Anthropic may one day stop serving — never a second authority
26
+ to keep in step.
21
27
 
22
28
  ## On disk now
23
29
 
24
- The whole published span `2.1.210` `2.1.257` 2.1.230, 2.1.244, 2.1.249 and
25
- 2.1.253–2.1.256 are not on the release channel one pristine binary per
26
- version, 41 in all. It straddles the **2.1.242 code split** (INTERNALS.md):
27
- `2.1.242`/`2.1.243` are the first many-module builds, and they jump ~35 MB over
28
- `2.1.241` for it. Several pairs share a byte count
29
- (`2.1.225`/`2.1.226`, `2.1.229`/`2.1.231`, `2.1.239`–`2.1.241`, `2.1.242`/`2.1.243`,
30
- `2.1.248`/`2.1.250`, `2.1.251`/`2.1.252`)
31
- and are still distinct binaries, which is why the identity column is the hash (of
32
- the whole file, `sha256sum <version>.orig`) and never the size:
30
+ One row per pristine binary held, appended as builds ship and never restated — so
31
+ a hash that stops matching means the file changed under you, not that a number
32
+ moved. A gap in the numbering is upstream's rather than a miss: not every version
33
+ reached the release channel, and `corpus.py status` names which, so no list of
34
+ them is kept here to go stale. The set straddles the **2.1.242 code split**
35
+ (INTERNALS.md): `2.1.242`/`2.1.243` are the first many-module builds, and they
36
+ jump ~35 MB over `2.1.241` for it. Sizes repeat across builds, which is why the
37
+ identity column is the hash (of the whole file, `sha256sum <version>.orig`) and
38
+ never the size:
33
39
 
34
40
  | version | size | sha256 |
35
41
  |---|---|---|
@@ -68,26 +74,63 @@ the whole file, `sha256sum <version>.orig`) and never the size:
68
74
  | `2.1.243` | 378 MB | `4b0dafeedd0b469c41988e200036fd773e7553ba960349c9f02a82c6d1f2ba27` |
69
75
  | `2.1.245` | 392 MB | `16ad2b94deaf7b29abed966d981c9991a47af0420f5be8ed4a3f83bea9f678bc` |
70
76
  | `2.1.246` | 248 MB | `1a0a662dc1bb938eaec38545abce9a4a69113d7d7f7c5e1a553ea276617b906a` |
71
- | `2.1.247` | 239 MB | `5fb321bf417ffc5cd4e3f36e7c9c7e029bf47aaa36d5621db979fcc5e6eabe15` |
72
- | `2.1.248` | 214 MB | `3edee3cb054bd6823674fd60d5c0e442825b28ee8fbf815af2d16bf0de072e16` |
73
- | `2.1.250` | 214 MB | `2be252a00ac56e704d7fbf7e5e9ef1243584093334a861945238a0c27e84bdac` |
74
- | `2.1.251` | 205 MB | `fd5f10ff0eb58daec04900466b143ea98aab50abf208a422bc008eaec13f61f7` |
75
- | `2.1.252` | 205 MB | `a715a45105e593fc9808d035d77781f88480b9897975a9df41837f0c591bd4b3` |
76
- | `2.1.257` | 206 MB | `9a64bda9d8722a1fa05bef9a5961d07e0331b99597eda9e2f6a732f3a0ff7f05` |
77
+ | `2.1.247` | 250 MB | `5fb321bf417ffc5cd4e3f36e7c9c7e029bf47aaa36d5621db979fcc5e6eabe15` |
78
+ | `2.1.248` | 224 MB | `3edee3cb054bd6823674fd60d5c0e442825b28ee8fbf815af2d16bf0de072e16` |
79
+ | `2.1.250` | 224 MB | `2be252a00ac56e704d7fbf7e5e9ef1243584093334a861945238a0c27e84bdac` |
80
+ | `2.1.251` | 214 MB | `fd5f10ff0eb58daec04900466b143ea98aab50abf208a422bc008eaec13f61f7` |
81
+ | `2.1.252` | 214 MB | `a715a45105e593fc9808d035d77781f88480b9897975a9df41837f0c591bd4b3` |
82
+ | `2.1.257` | 215 MB | `9a64bda9d8722a1fa05bef9a5961d07e0331b99597eda9e2f6a732f3a0ff7f05` |
83
+ | `2.1.258` | 215 MB | `704f1334ac65d3e89e1c6c1d7663293ad786a6166afdb71b5075337df630f976` |
84
+ | `2.1.259` | 217 MB | `f7dd62ae415378018cd21dd950eb3bac174ab085830304d3b8b098146bfd47b6` |
85
+ | `2.1.260` | 215 MB | `7a2fdc74b6836ea3d183f665b869f0ee3baebc9713cbebffe5838da4ea7bd82e` |
86
+ | `2.1.261` | 216 MB | `4ae40dd1784e85753e742e09f267d29ecbb82890361ad3817d27560866d364a6` |
87
+ | `2.1.263` | 216 MB | `26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba` |
88
+ | `2.1.265` | 216 MB | `e14738e3a58d1fc6ccc23b9c919451b4846bc27074a3fb48db976a7d595bdeeb` |
89
+ | `2.1.266` | 216 MB | `19842705e989393fce936804df6d2ab034860e24b8f8880357981d87ffd83fac` |
90
+ | `2.1.267` | 217 MB | `0399c793ff571d5946ef923d80b4f330d05ac4b6842a6b0775468f5d389403c0` |
91
+ | `2.1.268` | 219 MB | `9691a2b7bd796712ca8cffb8e32e54ff7fc45b662540233171a16a94a0425653` |
92
+ | `2.1.269` | 220 MB | `25e44883f54419569a3d739f38cbbdaebe83b09895da0f343e1b003710a4775b` |
93
+ | `2.1.270` | 224 MB | `3a624a5a7cd79bbad4d32bd7db36f1197ecf458bc5bf1e2aed81834a01ad3ef0` |
94
+ | `2.1.271` | 227 MB | `5e7b6fc24d0e124f68e99a0641c47b662945c6d197d768d7ad7a72cbf1897f58` |
95
+ | `2.1.272` | 227 MB | `d81396a668eb76fbddb49a2a5841f1b5d7af96b4c1f6500ced92f2c988f5bcd4` |
96
+ | `2.1.273` | 229 MB | `6c752e2cc7c110c9df15f26d8d134d438c5ae95dbd610efc1a308bf7f9c5f6c1` |
97
+ | `2.1.274` | 231 MB | `15e2d05148f801b5774032faad87e624ecd172e9903288bda448b892eb58fa07` |
98
+ | `2.1.275` | 232 MB | `13586f3150a7ca1655f36e1dba759fb404e0f7cf7021d4a3dcd5e6f604e56156` |
99
+ | `2.1.276` | 232 MB | `8a56c8a14bd3cb246e2bdb7e60aefe0f609bff78c8bbcc5ea6b1817c111c6145` |
100
+ | `2.1.277` | 234 MB | `722210f05ba494d8f6df69423c4d4f2960900f7a007d0532851c7a36e375cab7` |
101
+ | `2.1.278` | 234 MB | `5c4735937844e84f8a93306e841a5b0e12252909b07870f789b190468da147ab` |
77
102
 
78
103
  This set covers the span the playbook's tree-move measurements were taken over
79
104
  (`2.1.210` → `2.1.233`), the 2.1.242 split, the 2.1.246 stream-store
80
- migration, the 2.1.247 selector reads it was read back through, and the
105
+ migration, the 2.1.247 selector reads it was read back through, the
81
106
  2.1.257 compiled transcript renderer that retired live thinking's render half,
82
- so each is re-checkable here rather than historical.
107
+ and the 2.1.269 Bun 1.4.3 records that retired the container's record walker, so
108
+ each is re-checkable here rather than historical.
83
109
 
84
- ## Rebuild or extend it
110
+ ## Keep it complete
85
111
 
86
- The corpus grows every time patch-cc touches a new build — each first patch of
87
- a version leaves its `.orig` and a version this machine never patched joins
88
- the same way: save its pristine native binary as
89
- `~/.local/share/patch-cc/backups/<version>.orig`. To read the JS a given binary
90
- carries without patching anything:
112
+ ```bash
113
+ uv run scripts/corpus.py # what upstream serves, against what is held
114
+ uv run scripts/corpus.py sync # fetch every published build the set lacks
115
+ ```
116
+
117
+ `status` is read-only and exits non-zero when the set is short or damaged. It
118
+ keeps five answers apart rather than counting them together ([CONDUCT.md](CONDUCT.md)):
119
+ `ok`, `missing` (served, not held), `unpublished` (never served — a gap, not a
120
+ miss), `unverifiable` (held, no longer served, so nothing left to check it
121
+ against) and `corrupt` (held, and not what upstream shipped). `sync` acts on
122
+ `missing` alone: a `corrupt` entry is evidence that something here damaged a
123
+ binary, and replacing it silently would destroy the only sign of that — delete it
124
+ and re-run to take a fresh copy.
125
+
126
+ The span walked is the oldest build held → the channel's head, so no version
127
+ number is written down anywhere; `--since` moves the floor, and builds already
128
+ held are read, tabled and swept whatever the span says. A machine holding
129
+ nothing has made no statement about how far back it cares, so the walk starts at
130
+ the head — `--since` is how a fresh checkout asks for history. A build this machine
131
+ never patched can still join the old way — save its pristine native binary as
132
+ `~/.local/share/patch-cc/backups/<version>.orig` — and the next run verifies it
133
+ like any other. To read the JS a given binary carries without patching anything:
91
134
 
92
135
  ```bash
93
136
  patch-cc extract ~/.local/share/patch-cc/backups/2.1.233.orig > 2.1.233.js