patch-cc 0.5.1__tar.gz → 0.6.0__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.
- {patch_cc-0.5.1 → patch_cc-0.6.0}/PKG-INFO +13 -12
- {patch_cc-0.5.1 → patch_cc-0.6.0}/README.md +12 -11
- {patch_cc-0.5.1 → patch_cc-0.6.0}/docs/CONDUCT.md +28 -4
- patch_cc-0.6.0/docs/INTERNALS.md +273 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/docs/PLAYBOOK.md +246 -288
- {patch_cc-0.5.1 → patch_cc-0.6.0}/docs/corpus.md +64 -23
- patch_cc-0.6.0/scripts/corpus.py +370 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/__init__.py +1 -1
- patch_cc-0.6.0/src/patch_cc/bun/blob.py +286 -0
- patch_cc-0.6.0/src/patch_cc/bun/container.py +204 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/bun/elf.py +5 -10
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/bun/macho.py +4 -9
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/cli.py +3 -11
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/doctor.py +3 -4
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/js.py +10 -9
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/menu.py +2 -2
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patcher.py +9 -2
- patch_cc-0.6.0/src/patch_cc/patches/streaming.py +514 -0
- patch_cc-0.5.1/docs/INTERNALS.md +0 -266
- patch_cc-0.5.1/src/patch_cc/bun/blob.py +0 -544
- patch_cc-0.5.1/src/patch_cc/bun/container.py +0 -276
- patch_cc-0.5.1/src/patch_cc/patches/streaming.py +0 -1680
- {patch_cc-0.5.1 → patch_cc-0.6.0}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/.github/workflows/release-watch.yml +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/.github/workflows/release.yml +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/.gitignore +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/AGENTS.md +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/CLAUDE.md +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/LICENSE +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/docs/demo.gif +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/pyproject.toml +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/codex/gateway.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/codex/oauth.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/__init__.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/agents.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/base.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/chrome.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/codex.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/patches/thinking.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/src/patch_cc/ui.py +0 -0
- {patch_cc-0.5.1 → patch_cc-0.6.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patch-cc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
-

|
|
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
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-

|
|
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
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.
|
|
@@ -59,8 +59,32 @@ shape that already absorbs every case, found rather than bolted on.
|
|
|
59
59
|
not guesswork: declare it (`Outcome.declare`) so a green tick cannot cover a dead
|
|
60
60
|
feature. A step nobody declared is a step that cannot report its own death:
|
|
61
61
|
`branding` carried none, so a name badge that stopped being a bold render was
|
|
62
|
-
a green run, an unchanged banner, and a manifest asserting the new name.
|
|
63
|
-
|
|
62
|
+
a green run, an unchanged banner, and a manifest asserting the new name. And a
|
|
63
|
+
step answers for its own identity alone, never for a name another step
|
|
64
|
+
discovered: `live-thinking`'s reducer once skipped itself for want of a helper
|
|
65
|
+
the memo step had found, and eight required steps read *found nothing* over
|
|
66
|
+
one memo that had merely moved (2.1.257). See [PLAYBOOK.md](PLAYBOOK.md).
|
|
67
|
+
|
|
68
|
+
- **Ride what upstream ships working.** When the binary already does for one
|
|
69
|
+
kind of thing what a patch wants for another — live tool uses, for live
|
|
70
|
+
thinking — put the patch's data on that path instead of building a twin of
|
|
71
|
+
it. Every step of a parallel implementation is a claim only the patch needs,
|
|
72
|
+
and upstream owes it nothing; the path upstream ships is one it cannot break
|
|
73
|
+
without paying for it. `live-thinking`'s render half broke five times on
|
|
74
|
+
React idioms before it was cut to three insertions on the tool-use list
|
|
75
|
+
([PLAYBOOK.md](PLAYBOOK.md#live-thinking--streamingpy)).
|
|
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.
|
|
64
88
|
|
|
65
89
|
- **Port faithfully.** When you change a patch, verify its output against a real
|
|
66
90
|
bundle — byte-identical where behaviour must not change. `doctor` over the
|
|
@@ -72,5 +96,5 @@ shape that already absorbs every case, found rather than bolted on.
|
|
|
72
96
|
- **The user controls commits and releases.** Don't commit, push, or publish
|
|
73
97
|
unless asked.
|
|
74
98
|
|
|
75
|
-
The binary format, and why the
|
|
76
|
-
|
|
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.
|