patch-cc 0.1.4__tar.gz → 0.2.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.
Files changed (41) hide show
  1. {patch_cc-0.1.4 → patch_cc-0.2.0}/.github/workflows/ci.yml +1 -1
  2. {patch_cc-0.1.4 → patch_cc-0.2.0}/.gitignore +4 -0
  3. patch_cc-0.2.0/AGENTS.md +18 -0
  4. patch_cc-0.2.0/CLAUDE.md +1 -0
  5. {patch_cc-0.1.4 → patch_cc-0.2.0}/PKG-INFO +77 -10
  6. {patch_cc-0.1.4 → patch_cc-0.2.0}/README.md +74 -8
  7. {patch_cc-0.1.4 → patch_cc-0.2.0}/docs/INTERNALS.md +19 -3
  8. {patch_cc-0.1.4 → patch_cc-0.2.0}/docs/PLAYBOOK.md +176 -13
  9. {patch_cc-0.1.4 → patch_cc-0.2.0}/pyproject.toml +3 -2
  10. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/__init__.py +1 -1
  11. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/bun/blob.py +8 -3
  12. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/bun/container.py +10 -0
  13. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/bun/macho.py +0 -12
  14. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/cache.py +25 -1
  15. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/cli.py +437 -35
  16. patch_cc-0.2.0/src/patch_cc/codex/__init__.py +62 -0
  17. patch_cc-0.2.0/src/patch_cc/codex/gateway.py +407 -0
  18. patch_cc-0.2.0/src/patch_cc/codex/models.py +334 -0
  19. patch_cc-0.2.0/src/patch_cc/codex/oauth.py +361 -0
  20. patch_cc-0.2.0/src/patch_cc/codex/translate.py +855 -0
  21. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/doctor.py +29 -8
  22. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/locate.py +29 -17
  23. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/menu.py +495 -68
  24. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patcher.py +99 -45
  25. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/__init__.py +12 -12
  26. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/agents.py +44 -8
  27. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/base.py +43 -17
  28. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/chrome.py +16 -9
  29. patch_cc-0.2.0/src/patch_cc/patches/codex.py +523 -0
  30. patch_cc-0.2.0/src/patch_cc/ui.py +108 -0
  31. {patch_cc-0.1.4 → patch_cc-0.2.0}/uv.lock +1 -1
  32. patch_cc-0.1.4/src/patch_cc/ui.py +0 -69
  33. {patch_cc-0.1.4 → patch_cc-0.2.0}/.github/workflows/release.yml +0 -0
  34. {patch_cc-0.1.4 → patch_cc-0.2.0}/LICENSE +0 -0
  35. {patch_cc-0.1.4 → patch_cc-0.2.0}/docs/CONDUCT.md +0 -0
  36. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/bun/__init__.py +0 -0
  37. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/bun/elf.py +0 -0
  38. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/bun/errors.py +0 -0
  39. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/output.py +0 -0
  40. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/streaming.py +0 -0
  41. {patch_cc-0.1.4 → patch_cc-0.2.0}/src/patch_cc/patches/thinking.py +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- python-version: ["3.11", "3.12", "3.13"]
14
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
15
15
  steps:
16
16
  - uses: actions/checkout@v4
17
17
 
@@ -20,3 +20,7 @@ build/
20
20
  *.patched
21
21
  content*.js
22
22
  claude-*.bin
23
+ .opencode/
24
+
25
+ # Build journal, not documentation (see docs/PLAYBOOK.md for the shipped "why")
26
+ CODEX-PLAN.md
@@ -0,0 +1,18 @@
1
+ # patch-cc
2
+
3
+ Interactive patcher for the Claude Code native binary. What it does and how to
4
+ use it: [README.md](README.md).
5
+
6
+ Before changing anything, read [docs/CONDUCT.md](docs/CONDUCT.md) — how we
7
+ build here: the mindset, the guardrails, and who commits (the user does). Then:
8
+
9
+ - [docs/PLAYBOOK.md](docs/PLAYBOOK.md) — matcher rules, the patch reference,
10
+ how to repair a patch after a Claude update, and the map of the bundle's
11
+ native surfaces (the model registry and its consumers).
12
+ - [docs/INTERNALS.md](docs/INTERNALS.md) — the Bun container format and how
13
+ the binary is rewritten in place.
14
+
15
+ Verify with `uv run patch-cc doctor` (every patch against a clean bundle;
16
+ point it at the pristine copies in `~/.local/share/patch-cc/backups/` to sweep
17
+ older builds). There is no test suite by design — doctor against real bundles
18
+ is the check.
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patch-cc
3
- Version: 0.1.4
3
+ Version: 0.2.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
@@ -8,7 +8,7 @@ Project-URL: Issues, https://github.com/anfreire/patch-cc/issues
8
8
  Author-email: André Freire Ferreira <anfreire.dev@gmail.com>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
- Keywords: bun,claude,claude-code,cli,patch,tui
11
+ Keywords: bun,claude,claude-code,cli,codex,gpt,openai,patch,tui
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
21
22
  Classifier: Topic :: Software Development
22
23
  Classifier: Topic :: Utilities
23
24
  Requires-Python: >=3.11
@@ -52,12 +53,12 @@ uvx patch-cc # fullscreen menu, no install needed
52
53
  ```
53
54
 
54
55
  The menu is a single centered panel: move with `↑ ↓`, toggle with `space`,
55
- press `s` to save. Patches that carry a setting — subagent models, the startup
56
- name, the `--version` marker — open a centered modal on `enter`, and the row
57
- then shows what you chose. Everything choosable is a picker: the agent names
58
- and model aliases are **discovered from your binary itself**, so the menu can
59
- never offer something your build would reject. Typing exists only for the two
60
- genuinely free-text values.
56
+ press `s` to save. Patches that carry a setting — subagent models, Codex models,
57
+ the startup name, the `--version` marker — open a centered modal on `enter`, and
58
+ the row then shows what you chose. Everything choosable is a picker: the agent
59
+ names and model aliases are **discovered from your binary itself** (and the
60
+ Codex ones from your plan), so the menu can never offer something your build
61
+ would reject. Typing exists only for the genuinely free-text values.
61
62
 
62
63
  A patched binary records what was applied inside itself, so the menu always
63
64
  comes up showing the real current state, and `patch-cc status` answers
@@ -84,6 +85,7 @@ patch-cc # then just run it
84
85
  | Chrome | Disable spinner tips | No rotating tips on the spinner |
85
86
  | | Custom startup name | Defaults to `<your username>'s Code` |
86
87
  | | Mark `--version` | Appends `(patched)` — or any marker you choose |
88
+ | Codex | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
87
89
 
88
90
  ## Usage
89
91
 
@@ -97,6 +99,7 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
97
99
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
98
100
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
99
101
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
102
+ uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
100
103
  uvx patch-cc apply --from-cache # replay your last remembered selection
101
104
  uvx patch-cc status # exactly what is applied
102
105
  uvx patch-cc doctor # do all patches match this build?
@@ -105,8 +108,67 @@ uvx patch-cc list # every patch, described
105
108
  uvx patch-cc restore # put the original back
106
109
  ```
107
110
 
108
- `--model` and `--brand` imply their patches; agents and models are validated
109
- against what your installed binary actually ships.
111
+ `--brand`, `--model` and `--codex` imply their patches; agents and models are
112
+ validated against what your installed binary actually ships, and Codex model ids
113
+ against what your plan offers. `apply --help` lists all three.
114
+
115
+ ## Codex models
116
+
117
+ Bring your **ChatGPT/Codex-plan** models (GPT-5.x) into Claude Code and use them
118
+ alongside your Claude ones — in the `/model` picker, as a subagent override,
119
+ with `/effort` driving how hard they think. It is two halves: a patch that
120
+ teaches your binary to accept and route the models you pick, and a small
121
+ localhost **gateway** that translates between Claude Code and OpenAI. Only the
122
+ models you pick are diverted — every Anthropic request stays byte-identical,
123
+ so your Claude plan is untouched. Nothing extra to install: the gateway is pure
124
+ Python and ships with patch-cc; you just sign in to a ChatGPT or Codex plan.
125
+
126
+ ```bash
127
+ uvx patch-cc codex login # sign in to your ChatGPT/Codex plan
128
+ uvx patch-cc # Codex models → pick models → save with `s`
129
+ uvx patch-cc codex serve # start the gateway; keep it running
130
+ ```
131
+
132
+ Which models you want is a patch setting like the startup name, so the menu's
133
+ **Codex models** row is where you pick them — it lists what your plan offers,
134
+ live. Everything the menu does the command line does too:
135
+
136
+ ```bash
137
+ uvx patch-cc apply --codex gpt-5.6-sol --codex gpt-5.5
138
+ uvx patch-cc apply --help # lists the model ids your plan offers
139
+ uvx patch-cc apply --from-cache # replay your last selection
140
+ ```
141
+
142
+ Then pick a model like any other:
143
+
144
+ ```bash
145
+ claude --model sol # shortcut → newest gpt-5.6-sol
146
+ claude --model gpt-5.6-sol # the full id also works, as does /model
147
+ ```
148
+
149
+ - **Shortcuts** (`sol`, `terra`, `luna`, …) are the last word of the model id;
150
+ the newest in a family wins, the way `opus` means the latest Claude Opus. They
151
+ are derived from the ids you picked — there is nothing to configure.
152
+ - **Your binary is the record.** The models and the port live in the patched
153
+ binary, so `patch-cc status` names exactly what is registered, and
154
+ `codex serve` finds the right port with nothing to tell it.
155
+ - **They read as native everywhere.** A registered model is added to the
156
+ binary's own model table, so its real name shows in the status line and the
157
+ welcome banner, its plan-reported effort levels are declared, and
158
+ `/advisor sol` works like any other model. Ask for an effort the model
159
+ doesn't run and the gateway quietly runs the closest lower one — the same
160
+ clamping Claude Code documents for its own models.
161
+ - **The gateway has to be running.** Every surface that names it says whether it
162
+ is — the apply report as soon as you bake, `patch-cc status`, and `codex
163
+ status` — so you never learn it from a request that hangs instead.
164
+ - The gateway holds *your* OpenAI token and listens only on localhost. No
165
+ Anthropic-model request is ever diverted to it. The Codex ones that *are*
166
+ still carry Claude Code's own auth header — the gateway ignores it and never
167
+ forwards it, but treat the port as trusted: whatever binds it first sees it.
168
+ - After a Claude update, **re-bake** — the patch reverts with the binary — but
169
+ the gateway is separate and keeps running. Until you do, a Codex model you had
170
+ saved as your default reads as unavailable; the models live in the patch.
171
+ Change the port and the gateway needs a restart to follow it.
110
172
 
111
173
  ## After a Claude update
112
174
 
@@ -132,6 +194,8 @@ binary is *smaller* than the original (≈113 MB vs 267 MB), not larger.
132
194
 
133
195
  See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
134
196
  [docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
197
+ Changing anything here starts at [docs/CONDUCT.md](docs/CONDUCT.md) — how this
198
+ is built, and what a patch has to prove before it ships.
135
199
 
136
200
  ## Credits
137
201
 
@@ -139,6 +203,9 @@ The patch set is a Python port of
139
203
  [a-connoisseur/patch-claude-code](https://github.com/a-connoisseur/patch-claude-code),
140
204
  with the subagent-model override idea from
141
205
  [aleks-apostle/claude-code-patches](https://github.com/aleks-apostle/claude-code-patches).
206
+ Registering Codex models inside the bundle follows
207
+ [clodex](https://github.com/gxjansen/clodex); the routing here is done in the
208
+ bundle rather than with clodex's TLS interception.
142
209
 
143
210
  ## License
144
211
 
@@ -24,12 +24,12 @@ uvx patch-cc # fullscreen menu, no install needed
24
24
  ```
25
25
 
26
26
  The menu is a single centered panel: move with `↑ ↓`, toggle with `space`,
27
- press `s` to save. Patches that carry a setting — subagent models, the startup
28
- name, the `--version` marker — open a centered modal on `enter`, and the row
29
- then shows what you chose. Everything choosable is a picker: the agent names
30
- and model aliases are **discovered from your binary itself**, so the menu can
31
- never offer something your build would reject. Typing exists only for the two
32
- genuinely free-text values.
27
+ press `s` to save. Patches that carry a setting — subagent models, Codex models,
28
+ the startup name, the `--version` marker — open a centered modal on `enter`, and
29
+ the row then shows what you chose. Everything choosable is a picker: the agent
30
+ names and model aliases are **discovered from your binary itself** (and the
31
+ Codex ones from your plan), so the menu can never offer something your build
32
+ would reject. Typing exists only for the genuinely free-text values.
33
33
 
34
34
  A patched binary records what was applied inside itself, so the menu always
35
35
  comes up showing the real current state, and `patch-cc status` answers
@@ -56,6 +56,7 @@ patch-cc # then just run it
56
56
  | Chrome | Disable spinner tips | No rotating tips on the spinner |
57
57
  | | Custom startup name | Defaults to `<your username>'s Code` |
58
58
  | | Mark `--version` | Appends `(patched)` — or any marker you choose |
59
+ | Codex | Codex models | Use OpenAI/Codex-plan models in Claude Code — see [Codex models](#codex-models) |
59
60
 
60
61
  ## Usage
61
62
 
@@ -69,6 +70,7 @@ uvx patch-cc apply --brand # + branding as <username>'s Code
69
70
  uvx patch-cc apply --brand "Ada's Code" # + branding, explicit name
70
71
  uvx patch-cc apply --model Explore=haiku --model general-purpose=opus
71
72
  uvx patch-cc apply --suffix "(mine)" # custom --version marker
73
+ uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
72
74
  uvx patch-cc apply --from-cache # replay your last remembered selection
73
75
  uvx patch-cc status # exactly what is applied
74
76
  uvx patch-cc doctor # do all patches match this build?
@@ -77,8 +79,67 @@ uvx patch-cc list # every patch, described
77
79
  uvx patch-cc restore # put the original back
78
80
  ```
79
81
 
80
- `--model` and `--brand` imply their patches; agents and models are validated
81
- against what your installed binary actually ships.
82
+ `--brand`, `--model` and `--codex` imply their patches; agents and models are
83
+ validated against what your installed binary actually ships, and Codex model ids
84
+ against what your plan offers. `apply --help` lists all three.
85
+
86
+ ## Codex models
87
+
88
+ Bring your **ChatGPT/Codex-plan** models (GPT-5.x) into Claude Code and use them
89
+ alongside your Claude ones — in the `/model` picker, as a subagent override,
90
+ with `/effort` driving how hard they think. It is two halves: a patch that
91
+ teaches your binary to accept and route the models you pick, and a small
92
+ localhost **gateway** that translates between Claude Code and OpenAI. Only the
93
+ models you pick are diverted — every Anthropic request stays byte-identical,
94
+ so your Claude plan is untouched. Nothing extra to install: the gateway is pure
95
+ Python and ships with patch-cc; you just sign in to a ChatGPT or Codex plan.
96
+
97
+ ```bash
98
+ uvx patch-cc codex login # sign in to your ChatGPT/Codex plan
99
+ uvx patch-cc # Codex models → pick models → save with `s`
100
+ uvx patch-cc codex serve # start the gateway; keep it running
101
+ ```
102
+
103
+ Which models you want is a patch setting like the startup name, so the menu's
104
+ **Codex models** row is where you pick them — it lists what your plan offers,
105
+ live. Everything the menu does the command line does too:
106
+
107
+ ```bash
108
+ uvx patch-cc apply --codex gpt-5.6-sol --codex gpt-5.5
109
+ uvx patch-cc apply --help # lists the model ids your plan offers
110
+ uvx patch-cc apply --from-cache # replay your last selection
111
+ ```
112
+
113
+ Then pick a model like any other:
114
+
115
+ ```bash
116
+ claude --model sol # shortcut → newest gpt-5.6-sol
117
+ claude --model gpt-5.6-sol # the full id also works, as does /model
118
+ ```
119
+
120
+ - **Shortcuts** (`sol`, `terra`, `luna`, …) are the last word of the model id;
121
+ the newest in a family wins, the way `opus` means the latest Claude Opus. They
122
+ are derived from the ids you picked — there is nothing to configure.
123
+ - **Your binary is the record.** The models and the port live in the patched
124
+ binary, so `patch-cc status` names exactly what is registered, and
125
+ `codex serve` finds the right port with nothing to tell it.
126
+ - **They read as native everywhere.** A registered model is added to the
127
+ binary's own model table, so its real name shows in the status line and the
128
+ welcome banner, its plan-reported effort levels are declared, and
129
+ `/advisor sol` works like any other model. Ask for an effort the model
130
+ doesn't run and the gateway quietly runs the closest lower one — the same
131
+ clamping Claude Code documents for its own models.
132
+ - **The gateway has to be running.** Every surface that names it says whether it
133
+ is — the apply report as soon as you bake, `patch-cc status`, and `codex
134
+ status` — so you never learn it from a request that hangs instead.
135
+ - The gateway holds *your* OpenAI token and listens only on localhost. No
136
+ Anthropic-model request is ever diverted to it. The Codex ones that *are*
137
+ still carry Claude Code's own auth header — the gateway ignores it and never
138
+ forwards it, but treat the port as trusted: whatever binds it first sees it.
139
+ - After a Claude update, **re-bake** — the patch reverts with the binary — but
140
+ the gateway is separate and keeps running. Until you do, a Codex model you had
141
+ saved as your default reads as unavailable; the models live in the patch.
142
+ Change the port and the gateway needs a restart to follow it.
82
143
 
83
144
  ## After a Claude update
84
145
 
@@ -104,6 +165,8 @@ binary is *smaller* than the original (≈113 MB vs 267 MB), not larger.
104
165
 
105
166
  See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
106
167
  [docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
168
+ Changing anything here starts at [docs/CONDUCT.md](docs/CONDUCT.md) — how this
169
+ is built, and what a patch has to prove before it ships.
107
170
 
108
171
  ## Credits
109
172
 
@@ -111,6 +174,9 @@ The patch set is a Python port of
111
174
  [a-connoisseur/patch-claude-code](https://github.com/a-connoisseur/patch-claude-code),
112
175
  with the subagent-model override idea from
113
176
  [aleks-apostle/claude-code-patches](https://github.com/aleks-apostle/claude-code-patches).
177
+ Registering Codex models inside the bundle follows
178
+ [clodex](https://github.com/gxjansen/clodex); the routing here is done in the
179
+ bundle rather than with clodex's TLS interception.
114
180
 
115
181
  ## License
116
182
 
@@ -88,10 +88,12 @@ edit is followed by an ad-hoc `codesign` (mandatory on Apple Silicon).
88
88
 
89
89
  ## The manifest
90
90
 
91
- Every patched bundle ends with a single comment line:
91
+ Every patched bundle ends with a single comment line — the one description of
92
+ its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it means for matcher health:
92
93
 
93
94
  ```
94
- //patch-cc {"v":1,"tool":"0.1.0","patches":[...],"brand":...,"models":{...}}
95
+ //patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"models":{...},
96
+ "codex":{"port":8817,"models":["gpt-5.6-sol", ...]}}
95
97
  ```
96
98
 
97
99
  That line is why `patch-cc status` can name exactly what is applied: several
@@ -100,6 +102,18 @@ can't collide with code and travels with the bundle through extract/repack.
100
102
  The menu also reads it to pre-select the current patch set — the binary is the
101
103
  state.
102
104
 
105
+ Every key after `patches` belongs to a patch and is written only when *that*
106
+ patch landed, so `status` can never assert a name, marker, or model the bundle
107
+ does not contain.
108
+
109
+ Each key records what was *asked for*, never what was derived from it. `codex`
110
+ carries model ids and a port and nothing else: a Codex model's display name and
111
+ context window are already baked into the bundle, and repeating them here would
112
+ be a second copy — one that a relabelling upstream could make disagree with the
113
+ binary it claims to describe. That is also what makes the manifest the single
114
+ home for the gateway port: `codex serve` and `codex status` read it from here
115
+ rather than from a store of their own.
116
+
103
117
  ## Safety
104
118
 
105
119
  - Before the first patch of a version, the pristine binary is copied to
@@ -112,4 +126,6 @@ state.
112
126
  - Every write is verified: patch-cc re-extracts the JS from the binary it just
113
127
  wrote and asserts it equals what it meant to write.
114
128
  - Patching a binary that is already marked, when no pristine backup exists, is
115
- refused unless `--force` — there is nothing clean to start from.
129
+ refused outright — there is nothing clean to start from, and our edits change
130
+ lengths, so a second pass would corrupt rather than update. `restore` or a
131
+ reinstall are the only honest fixes; there is deliberately no override.
@@ -43,27 +43,37 @@ Anything the binary can enumerate for us, it does:
43
43
  `"Internal` are plumbing and are not offered.
44
44
  - **Model aliases** come from the Task tool's own input schema: the
45
45
  `model:<zod>.enum([...])` whose describe-string starts
46
- `Optional model override` (`agents.discover_models`).
46
+ `Optional model override` `agents.MODEL_ENUM`, one compiled anchor because
47
+ `codex-models` splices the chosen Codex ids into the very group
48
+ `discover_models` reads back out.
47
49
 
48
50
  A new upstream agent or model appears in `patch-cc list`, the menu, and
49
51
  `--model` validation without a code change. If the enum anchor ever vanishes,
50
52
  discovery falls back to `haiku/sonnet/opus` — `doctor` prints both lists, so a
51
53
  missing agent or alias is visible at a glance.
52
54
 
53
- ## The manifest
54
-
55
- Every patched bundle ends with one comment line:
55
+ **The one hardcoded list, and why it may stay one.**
56
+ `models._RESERVED` names Claude's short built-ins so `apply --codex opus` is
57
+ refused, and so a *derived* family shortcut can never take one of those names
58
+ either (a pathological `gpt-5.6-opus` is the only way it could try). It is a
59
+ snapshot, so upstream can outgrow it — and it does not matter, because it is a
60
+ courtesy, not the guard. A name the bundle already knows makes `enum` and
61
+ `validator` find it *present*, so both rewrite nothing; they are `expect=True`,
62
+ so the patch reports broken, the fixpoint drops it, and nothing is written. The
63
+ bundle refuses the collision at bake time, derived rather than remembered.
64
+ Reserving the discovered set instead would buy a friendlier message for a hazard
65
+ that cannot land, at the price of reading a 275 MB binary before argument parsing
66
+ can finish.
56
67
 
57
- ```
58
- //patch-cc {"v":1,"tool":"0.1.0","patches":[...],"brand":...,"models":{...}}
59
- ```
68
+ ## The manifest
60
69
 
61
- A comment cannot collide with code, survives re-extraction, and makes `status`
62
- a parse instead of a guess several patches are value flips
63
- (`verbose:!0`) that leave no other fingerprint. `is_patched` also still
64
- recognises the legacy fingerprints (`__cc_` identifiers, the old `--version`
65
- marker) so binaries patched by pre-manifest versions are not mistaken for
66
- clean.
70
+ Every patched bundle ends with one comment line recording what was applied; its
71
+ shape lives in [INTERNALS.md](INTERNALS.md#the-manifest). What matters here is
72
+ that it makes `status` a parse instead of a guess several patches are value
73
+ flips (`verbose:!0`) that leave no other fingerprint — and that `is_patched`
74
+ also still recognises the legacy fingerprints (`__cc_` identifiers, the old
75
+ `--version` marker), so binaries patched by pre-manifest versions are not
76
+ mistaken for clean.
67
77
 
68
78
  ## How resilience is detected
69
79
 
@@ -308,6 +318,13 @@ anchor, and where it lives.
308
318
  definition (discovered as above): rewrite the `model:"..."` literal when the
309
319
  definition has one, insert `model:"...",` right after `agentType:"...",`
310
320
  when it doesn't. Both splice at offsets from a fresh discovery pass.
321
+ Every requested override is a **required step**: each one reaching this patch
322
+ has already been validated against the bundle by whichever surface asked for
323
+ it, so a pin that cannot be written is not a shape this build lacks — it is
324
+ the asked-for change failing, and the patch is dropped rather than shipping a
325
+ manifest that claims it. An override whose target the definition already
326
+ carries counts as landed: the step is judged on what it achieved, not on
327
+ whether bytes moved.
311
328
  **The bypass:** one helper ignores the definition's model for a single
312
329
  pinned agent (Explore today) — shape
313
330
  `function f(def,main){if(def.agentType!==X.agentType||def.source!=="built-in")return def.model;…;return g(main)?PIN:"inherit"}`.
@@ -338,6 +355,152 @@ anchor, and where it lives.
338
355
  build that pins two agents gets both handled, rewritten last-first so earlier
339
356
  offsets stay valid.
340
357
 
358
+ ### Codex — `codex.py`
359
+
360
+ - **`codex-models`** — make Claude Code accept and show the chosen Codex models,
361
+ and divert *only* those models' requests to the localhost gateway.
362
+ Eight sub-steps; with nothing chosen the patch is a no-op, like
363
+ `subagent-models`. The ids and the port are ordinary patch configuration
364
+ (`Options.codex_models` / `codex_port`) — chosen in the menu or with
365
+ `--codex`/`--codex-port`, never read from a store of the patch's own.
366
+
367
+ | step | what it changes | anchor |
368
+ |---|---|---|
369
+ | `enum`\* | the Task tool's `model` enum, so a subagent can be pinned to a Codex id | `agents.MODEL_ENUM` — the same anchor `discover_models` reads |
370
+ | `validator`\* | the known-model array — it gates *resolution*, not just acceptance | `["sonnet","opus","haiku","fable",…,"opusplan",…]` |
371
+ | `resolver`\* | the override resolver `J9n` | its `case"best":{…}` block |
372
+ | `general-resolver`\* | the resolver every ordinary request uses, `Ei` | `case"best":return X();default:}` |
373
+ | `redirect`\* | swaps the request origin to `127.0.0.1:<port>` | the SDK's `buildRequest`, up to `let u=this.buildURL(…)` |
374
+ | `picker` | the `/model` list | `?[n,r]:[r];for(let i of o)push(e,i,t);` |
375
+ | `context` | the real context window | the brace-free `(e,t)` body reading `CLAUDE_CODE_MAX_CONTEXT_TOKENS` |
376
+ | `registry` | the binary's own model table — the status-line name, effort capabilities, `/advisor` eligibility | `],aliases:{` closing the `models:[...]` registry |
377
+
378
+ \* required (`expect=True`) — without any one of them the feature is dead.
379
+ `picker`, `context` and `registry` are refinements: absent, you can still
380
+ type `/model <id>` and get the 200k default under the model's raw id.
381
+ `context` has no step at all when no chosen model reports a window — there
382
+ is no rewrite owed, and reporting that as either a missing shape or a missed
383
+ rewrite would blame the build for having nothing to do.
384
+
385
+ **An id is a model's whole identity.** There is no second name to carry: the id
386
+ is what `--codex` takes, what the manifest records, what the enum and the
387
+ picker and the redirect array hold, and what a diverted request already names
388
+ by the time the gateway sees it. Everything else is derived — the display name
389
+ and the window from the plan, the shortcut from the id — so nothing downstream
390
+ keeps a mapping that could disagree with the bundle. That is why the gateway
391
+ needs only the port, and it is structural rather than a rule to remember:
392
+ there is no map left to read.
393
+
394
+ **The picker's label rule is one rule.** Every row's label is its handle
395
+ *spelled as a name* — `_display_name` turns `sol` into `Sol` and
396
+ `gpt-5.6-sol` into `GPT 5.6 Sol` — because the binary's own rows are named,
397
+ not slugged. Labelling id rows with the raw id instead made them the only
398
+ entries in the list wearing a different sort of name than their neighbours
399
+ (`gpt-5.6-sol` sitting under `Opus`). Two label rules is what let that
400
+ through, so there is one.
401
+
402
+ **The registry is the binary's own model table, and the sharpest splice
403
+ here.** Everything Claude Code knows about a model it did not hardcode a
404
+ check for lives in one embedded object — `models:[{id, family, display_name,
405
+ provider_ids, context, capabilities:[...], default_effort, advisor_rank,
406
+ ...}]` plus the `aliases` map its resolvers read — validated by a zod
407
+ `safeParse` whose fallback is **empty** (`models:[]`): one malformed entry
408
+ strips every model, Claude's included, of its metadata. So the step emits
409
+ only fields the schema declares — the required four (`id`, `family`,
410
+ `display_name`, `provider_ids.first_party`) plus values with something true
411
+ to record. `default_effort` is deliberately not among them: the binary
412
+ resolves a missing default as `high` (`?.default_effort??"high"` — the very
413
+ default its own flagships declare), so omission makes `/effort auto` and an
414
+ untouched session mean on these models exactly what they mean on Opus,
415
+ instead of importing Codex-the-product's own default (`low` on sol). An
416
+ entry is what turns "accepted" into "first-class": the status line resolves
417
+ `display_name` through it (raw id without one), `/advisor` eligibility is
418
+ exactly "has an `advisor_rank`", the effort gates read `capabilities`, and
419
+ surfaces nobody has enumerated inherit the same answers. `/advisor sol` on a
420
+ freshly baked binary is the cheap end-to-end check that the entry parsed.
421
+
422
+ **What the entry deliberately leaves out.** `pricing` and
423
+ `max_output_tokens` (readers guard for absence; a subscription has no
424
+ per-token price; the cap never reaches the wire), `context` (the window
425
+ resolver ends on a flat 200k without consulting the registry — the `context`
426
+ step stays the one home for the window), and the `aliases` /
427
+ `latest_per_family` maps (shortcuts already resolve through the spliced
428
+ arms; a second mechanism would be a second home — and `latest_per_family`
429
+ feeds Claude's own system-prompt text). The `/model` picker does not iterate
430
+ the registry either — its rows are hand-built per family upstream, which is
431
+ why `picker` exists — and our row push is the same `{value,label,description}`
432
+ + `.some()` idiom the binary itself uses for `ANTHROPIC_CUSTOM_MODEL_OPTION`.
433
+
434
+ **Capabilities can only say yes.** The binary reads an *absent* capability
435
+ as "ask the provider fallback", which on the first-party API is permissive —
436
+ upstream's own choice for models it does not know. That is why the `/effort`
437
+ menu offers the whole ladder on any imported model, and why an entry cannot
438
+ *hide* a level; hiding would mean splicing the per-level exclusion chains,
439
+ a new matcher surface bought for a cosmetic win. Which levels a model
440
+ actually runs is the backend's per-model ruling, refused with a structured
441
+ 400 (`param:"reasoning.effort"`, `code:"invalid_value"`) before any
442
+ generation — measured: gpt-5.5 runs `xhigh` and refuses `max`. Baking that
443
+ ruling would be a copy free to go stale between bake and runtime, so the
444
+ gateway clamps off the refusal itself (`translate.clamp_effort`): one rung
445
+ down per retry, mirroring Claude Code's documented "highest supported level
446
+ at or below" rule for its own models. A menu that over-offers costs one
447
+ extra round-trip, never a dead turn.
448
+
449
+ **It runs before `subagent-models`, and that ordering is load-bearing.**
450
+ `enum` registers the ids in the very schema `discover_models` reads, so a
451
+ subagent pinned to a Codex model is offered exactly when that model is really
452
+ in the bundle. Registering *after* meant `subagent-models` had to be told
453
+ about the ids out of band, so it landed a pin whether or not
454
+ `codex-models` did: drop `codex-models` for a drifted anchor and the binary
455
+ kept `model:"gpt-5.6-sol"` on an agent, pointing at a model nothing had
456
+ registered, with the manifest asserting the override. Now the dropped patch
457
+ takes its pins down with it.
458
+
459
+ **Two resolvers, and why both.** `Ei` is the general one — it turns `opus`
460
+ into `claude-opus-4-8`, its return value *replaces* the model before the
461
+ request is built, and it passes an unknown-but-valid name straight through
462
+ (`return e`). `J9n` runs only when managed `availableModels` are active and
463
+ defaults to `null`, not passthrough. An id needs neither arm (identity
464
+ *is* passthrough); a **family shortcut** (`sol` → the newest
465
+ `gpt-<ver>-sol`) needs one in both, and `Ei`'s is what makes it work on the
466
+ ordinary path.
467
+
468
+ **The shortcut gate.** Shortcuts are registered only when the `Ei` anchor is
469
+ present. Absent it, none are registered anywhere and the ids — which need none
470
+ of this — carry on. Accepted-but-unresolved is the failure worth engineering
471
+ against: it leaves the redirect (which matches ids only), reaches Anthropic as
472
+ an unknown model, and 404s.
473
+
474
+ **Why the "already added?" checks are bounded.** The resolver check matches
475
+ only the contiguous arms *at the insertion point* (`_RESOLVER_ARMS`), never
476
+ the whole bundle: a short word like `auto` occurs as `case"auto":return` in
477
+ stock code, so a global check would skip its arm while the id arms still
478
+ marked the step applied — a shortcut that resolves nowhere. The picker drops
479
+ its build-time check for the same reason and leans on the runtime `.some()`
480
+ guard it injects. `Ei`'s own idempotency is the `default:}` adjacency: it
481
+ matches the pristine arm only.
482
+
483
+ **Routing knows nothing about shortcuts.** The redirect tests `body.model`
484
+ against the baked id array, and the context table is keyed the same way — by
485
+ the time either runs, `Ei` has already rewritten the shortcut. Measured on the
486
+ wire: `claude --model sol` arrives at the gateway as `"gpt-5.6-sol"`. That is
487
+ why a drift in the shortcuts costs shortcuts and nothing else.
488
+
489
+ **A diverted request still carries Claude Code's auth header.** Measured, not
490
+ assumed: point a listener at the gateway port and a Codex turn arrives with
491
+ `Authorization: Bearer sk-ant-oat01-…` and the whole prompt. The gateway
492
+ ignores it and never forwards it, so the exposure is to whatever holds the
493
+ port. Stripping it from `redirect` does **not** work — don't retry it blind.
494
+ `options.headers` is the last source `buildHeaders` merges and its merge
495
+ treats `null` as delete, so setting a null (or an inert value) there ought to
496
+ win; neither reaches the wire. With the injected block proven to run — a
497
+ marker spliced into the replacement URL came through in the path — a probe
498
+ header set on the options object *and* on the local copy was absent from the
499
+ request both times, so something between `buildRequest` and `fetch` discards
500
+ `options.headers` on this build. A real fix needs its own anchor further
501
+ down, in `prepareRequest` (it receives the final `Headers` and the URL): a new
502
+ required step and new matcher surface, deliberately not taken for 0.2.0.
503
+
341
504
  ### Chrome & branding — `chrome.py`
342
505
 
343
506
  - **`spinner-tips`** — force spinner tips off. Anchors: `spinnerTipsEnabled===!1`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "patch-cc"
7
- version = "0.1.4"
7
+ version = "0.2.0"
8
8
  description = "Interactive patcher for the Claude Code native binary"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
13
13
  authors = [
14
14
  { name = "André Freire Ferreira", email = "anfreire.dev@gmail.com" },
15
15
  ]
16
- keywords = ["claude", "claude-code", "patch", "bun", "cli", "tui"]
16
+ keywords = ["claude", "claude-code", "codex", "openai", "gpt", "patch", "bun", "cli", "tui"]
17
17
  classifiers = [
18
18
  "Development Status :: 3 - Alpha",
19
19
  "Environment :: Console",
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Programming Language :: Python :: 3.11",
25
25
  "Programming Language :: Python :: 3.12",
26
26
  "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
27
28
  "Topic :: Software Development",
28
29
  "Topic :: Utilities",
29
30
  ]
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.2.0"
@@ -90,9 +90,14 @@ class Blob:
90
90
  return self.payload(self.entry_module().ranges["contents"])
91
91
 
92
92
  def bytecode_size(self) -> int:
93
- return (
94
- self.entry_module().ranges["bytecode"][1] if self.struct_size == 52 else 0
95
- )
93
+ """Bytes of precompiled bytecode on the entrypoint, for either layout.
94
+
95
+ ``bytecode`` is the fourth pair, so *both* record formats carry it --
96
+ ``FIELDS_OLD`` is the first four of ``FIELDS_NEW``. Reporting zero for the
97
+ 36-byte layout said "already stripped" about a module that had 154 MB of
98
+ it, which is the one thing ``status`` reads this for.
99
+ """
100
+ return self.entry_module().ranges["bytecode"][1]
96
101
 
97
102
 
98
103
  def _detect_struct_size(modules_len: int) -> int:
@@ -127,3 +127,13 @@ def verify(path: str, expected: str) -> None:
127
127
  "patched binary did not round-trip: extracted source differs from "
128
128
  f"what we wrote ({len(written.source):,} vs {len(expected):,} bytes)"
129
129
  )
130
+ if written.bytecode_size:
131
+ # Read back off the written file, not asserted in memory. Bun runs the
132
+ # bytecode in preference to the source, so any left behind would run the
133
+ # *unpatched* program while every check above agreed the source was ours
134
+ # -- every patch a silent no-op. docs/INTERNALS.md calls this the tripwire
135
+ # for a Bun that makes bytecode authoritative; this is where it trips.
136
+ raise ContainerError(
137
+ f"patched binary still carries {written.bytecode_size:,} bytes of "
138
+ "entrypoint bytecode, which would run instead of our edits"
139
+ )