Skip to content

Releases: anfreire/patch-cc

v0.6.0 — never move a pristine byte

Choose a tag to compare

@anfreire anfreire released this 12 Sep 15:07

Claude Code 2.1.269 moved to Bun 1.4.3, which chains two more records after the module table. patch-cc 0.5.x walked that chain with a whitelist of the records it knew, and refused the build outright (offsets flags 0x1bff carry record bits 0x1800 this code does not know). This release fixes that build, and fixes the class: the write no longer needs to know Bun's records at all.

What changed

  • The write never moves a pristine byte. Each edited module's text is appended after the arena and its stale bytecode is unlinked; everything else — the module table, Bun's records, whatever a future Bun adds — is copied where it was, and only the pointers that name what changed are re-aimed. The record walker, its whitelist and the compaction are gone.
  • Verification is byte-for-byte against the pristine blob, allowing exactly the intended edits and enumerating nothing, so a record patch-cc has never parsed is covered by the same comparison as one it has.
  • A patched binary is now a few percent larger than the original, not smaller (2.1.269: 220 → 229 MB). The size win was what bought the fragility. The apply report, the menu and patch-cc status say so plainly, and status reports the bytecode the module table names.

Compatibility

Transparent. Every held build from 2.1.210 to 2.1.269 patches with the same per-patch results as before. If Claude auto-updated you onto 2.1.269 and patch-cc refused it, upgrade and re-bake (patch-cc apply --from-cache, or re-run the menu). macOS is no longer the odd one out: growing a segment is the only thing the write asks of either container.

Validated

patch-cc doctor bakes and boots all 51 corpus builds, 2.1.210 → 2.1.269; every tail record on every build was checked to point into the arena; a real 2.1.269 apply boots, answers a turn, and runs its edited modules from inside Bun 1.4.3's pre-linked graph.

uvx patch-cc            # or: uv tool install patch-cc && patch-cc

v0.4.0 — code-split bundle support

Choose a tag to compare

@anfreire anfreire released this 25 Aug 02:21

Claude Code 2.1.242 turned on Bun code-splitting: the binary's entrypoint went from carrying the whole ~28 MB app to a ~20 KB argv shim that lazily imports the app across ~1,300 chunk-*.js modules. patch-cc assumed the patchable surface was that one entrypoint module, so on 2.1.242 and 2.1.243 twelve of thirteen patches matched nothing (only --version marking survived). This release fixes that.

What changed

  • The patchable surface is now every JavaScript module the binary declares, discovered off the entrypoint's own loader — not just the entrypoint. find/apply span every module, each edit routes to the module it came from, and a pre-split build is simply the one-module case of the same code. No new behaviour to configure.
  • The syntax gate reads past module-linkage the JS grammar can't model (reserved words as import/export aliases, which the split minifier emits) while still catching any splice that breaks real code.
  • A patched binary is smaller by exactly the modules it changed — the stale bytecode of edited modules is dropped, the rest keeps its fast start (83 MB smaller on 2.1.243).

Compatibility

Fully transparent. Pre-split builds (2.1.241 and earlier) patch byte-identically to before; 2.1.242+ now patch cleanly. If Claude auto-updated you onto 2.1.242 or 2.1.243 and your patches vanished, re-run patch-cc (or patch-cc apply --from-cache).

Validated

patch-cc doctor is green across the whole 2.1.210 → 2.1.243 backup corpus, and a real 2.1.243 apply lands all 13 patches, boots, and runs the edited source.

uvx patch-cc            # or: uv tool install patch-cc && patch-cc

patch-cc 0.3.3 — follow the state into the stream store

Choose a tag to compare

@anfreire anfreire released this 19 Aug 22:51

Repairs live thinking on Claude 2.1.236. On that build live-thinking
lost its required prop-threading step, so the fixpoint dropped the whole
patch and thinking stopped streaming mid-turn — every other patch landed.
Upgrade and re-bake (patch-cc apply --from-cache, or re-run the menu).

What broke

A semantic move, not a spelling: 2.1.236 took the streaming state out of the
App component's useState and put it in an external stream store
(subscribe/getSnapshot/_publish — the useSyncExternalStore shape),
handing the reducer <store>.setStreamingThinking instead of a bare setter.
The resolution knew only the shape every archived build had carried — the
array pattern binding the handed setter — so it read a build that plainly
holds the state as holding none. It failed the way it is built to: loudly, by
name, with every other patch landing and the binary intact.

Upstream is converging on the feature from below: the store names the field
streamingThinking, its setter takes functional updaters (the contract the
spliced updates already speak), it hides a finished block after 30 s — its own
linger — and the cancel path now preserves interrupted thinking. What it still
does not do is accumulate thinking_delta text: the delta arm feeds a token
counter, and nothing renders live. The patch is still the feature.

The fix

What the handed setter is names the state both ways. A bare identifier is a
useState setter and the state is its array-pattern sibling, as before. A
member read names its store, and the state is the snapshot pattern the same
scope destructures from the hook call it hands that store —
{streamingToolUses:…}=useX(<store>) — extended with upstream's own field
name, or read from upstream's own binding the day they thread it themselves.
The pattern is proven the snapshot read by the store expression as the call's
only argument, one answer or none (js.only) — a second argument is a
selector whose result is no longer the snapshot. The insertion is witnessed
against the bundle's own naming of the field (the snapshot initialiser, the
publish call), so a renamed field reports the store instead of threading
undefined with every count green.

final-summary moved homes on the same build — an experiment gate now sits
between the thinking test and the summary it guards — so its guard is found by
what its condition tests, never by being the nearest if, bounded by the
function that declares the block.

Also in this release

  • The corpus is the whole published span now: one pristine binary per
    version, 2.1.210 → 2.1.236 (2.1.230 never shipped), 26 in all — every
    tree-move measurement is re-checkable rather than historical
    (docs/corpus.md).
  • The skip notes fold into a count: 4 conversation render(s), 3 with no live-thinking state in scope — one line where 57 identical sentences per
    sweep said less, and the per-build number (2 renders threaded through
    2.1.222, 1 after) is legible at a glance.

Swept over all 26 pristine builds: 2.1.236 flips to green at cand=14
level with 2.1.235 — and no candidate count moves on any older build.
Verified at runtime on patched 2.1.236: thinking streams mid-turn, drawn
inline while the spinner still counts.

patch-cc 0.3.2 — repair the 2.1.235 identity drift

Choose a tag to compare

@anfreire anfreire released this 19 Aug 12:03

Repairs live thinking on Claude 2.1.235. On that build live-thinking
lost its required prop-threading step, so the fixpoint dropped the whole
patch and thinking stopped streaming mid-turn — every other patch landed.
Upgrade and re-bake (patch-cc apply --from-cache, or re-run the menu).

What broke

Nothing behavioural. 2.1.235 retired agentDefinitions from the
conversation-render props bags — the prop still occurs 97 times in the bundle,
as other components' prop. The matcher used it as witness and insertion point
in one, so an identity resting on a neighbour read a build that plainly drew
four conversation renders as drawing none, with every anchor count standing.
Third break of one class in eleven releases: 2.1.224 took the .enum( callee,
2.1.234 the resolver's exact return null, 2.1.235 the neighbouring prop —
each an identity conjunct beyond the semantic essence, kept only for as long
as upstream happened to keep it.

The fix

prop-threading's identity is now the pair that makes a render a conversation
render — conversationId and messages — carried by a bag handed to a
component
: an argument. The bound is part of what a render is, spelled as
grammar rather than as a neighbour's name; it is what keeps a module-level
literal, a return-value payload, or a config object carrying the pair from
being read as a render. Insertion sits before conversationId, one of the
identity's own props, so it cannot be absent from a bag the identity admitted.
Along the way:

  • transcript-signature sheds the same triple-role conjunct one prop over:
    showAllInTranscript was find-anchor, identity and insertion point in one,
    and discriminates nothing on any archived build — messages plus
    streamingToolUses names the same one renderer, so the pair is the identity
    and the insertion point moved inside it;
  • the pair-and-argument identity sees all four conversation renders — the
    neighbour-based one silently saw two.

Swept over all 13 archived pristine builds (2.1.216 → 2.1.235): 2.1.235 flips
to green at cand=14 — level with 2.1.234 — and no candidate count moves on
any older build. Verified at runtime on patched 2.1.234 and 2.1.235: thinking
streams mid-turn.

Also in this release

  • Declaring a step is the API's shape now, not a discipline.
    Outcome.declare(required=, optional=) is the only way a step comes to
    exist, and outcome.step(name) only retrieves — an undeclared name is a
    broken patch, a code path that never runs leaves a required step at 0/0
    with a verdict to fail, and a typo cannot mint a silently optional step.
    All five patch modules declare up front; live-thinking's dispatch points
    now fail by name on a build whose reducer is gone, instead of never
    having existed.
  • A verification card for create-diff in the PLAYBOOK: the paths that
    legitimately show no diff (Bash-created files, plan previews, condensed
    contexts, non-verbose scratchpad re-renders) are named next to the patch,
    so an "is it alive?" check by eye has an oracle. Measured on 2.1.234:
    every reachable path drew the diff.

patch-cc 0.3.1 — repair the 2.1.234 resolver drift

Choose a tag to compare

@anfreire anfreire released this 18 Aug 10:11

Repairs the Codex bridge on Claude 2.1.234. On that build codex-models
lost its required resolver step, so a bake shipped without your Codex models —
every other patch landed. Upgrade and re-bake (patch-cc apply --from-cache, or
re-run the menu).

What broke

Nothing behavioural. 2.1.234 reshaped the override resolver's default from
return null to

default:return vXu(e)?xVe(t):null   //  function vXu(e){return!1}

— a dead recognizer in front of the same rejection: vXu is a stub that always
answers !1, so the build rejects an unknown model exactly as 2.1.233 did. The
matcher asked "is the default's return exactly a bare null" and read the
added scaffolding as the resolver being gone, with every anchor count standing.

The fix

An identity now asks the weakest claim that still proves it — the membership
lesson props and arrays already taught (create-diff, max-effort, the
validator). _may_answer_null reads the default's possible answers off the
grammar's own value routing — a ternary answers with either branch, parentheses
and a sequence with their last expression, ||/?? with their right side,
&& with either — so null among them is the override resolver's rejection,
whatever upstream composes in front of it. Along the way:

  • every scoped return is read, fixing a latent bug beside the break: the old
    predicate took the first unscoped return, so a nested callback's
    return null could have answered for the default;
  • a case"best": arm counts only when its switch's labels carry the built-in
    models, so a throwaway arm elsewhere is nothing to splice or classify;
  • both resolver identities are asked positively and the complement is gone —
    an arm answering neither raises as one broken patch naming the new shape,
    where the complement absorbed it silently;
  • every green run prints resolvers: 1 override, 1 general, so a moved count
    is the early warning before anything breaks.

Swept over all 12 archived pristine builds (2.1.216 → 2.1.234): the same one
override and one general resolver on every build, 2.1.234 green at
cand=8 applied=8, and no other count moved.

v0.3.0 — the move to the parsed tree

Choose a tag to compare

@anfreire anfreire released this 16 Aug 00:11

patch-cc now locates and edits the Claude Code bundle through a parsed syntax tree instead of string and regex scanning. A patch finds an authored name — a property, a case label, an API string — and edits the grammar node it identifies, never the minified syntax between anchors, so a re-minified daily build changes nothing a matcher depends on.

Why upgrade now

  • Opens current Claude again. The released 0.2.x line reads the entrypoint by a hardcoded list of module names, which the 2.1.229 rename (…/src/entrypoints/cli.js…/cli) broke — 0.2.x opens no build from 2.1.229 on. 0.3.0 reads the entrypoint the container itself declares (entry_point_id), the same index Bun resolves by.

What changed

  • A parsed-tree toolkit (patch_cc/js.py): an immutable Source, node-boundary edits, and an incremental reparse that doubles as a syntax gate — every batch of edits is parsed, and rubble is refused before it can reach a binary.
  • Discovery over hardcoding. Built-in agents and model aliases are read from the bundle itself (including agentType values upstream hoists into constants), so a new upstream agent or model appears without a code change.
  • Safety hardening. A temporal-dead-zone-aware scope check (a spliced identifier can no longer be read before its declaration), a from-scratch parse of the final image before writing, an ELF guard that refuses an allocated SHT_NOBITS section after .bun, and deduplication of Codex model ids at the cache/manifest boundary.
  • A documented corpus. docs/corpus.md lists the pristine backups doctor sweeps, with hashes, so every measured claim in the playbook is one command from re-verification.

Verified with patch-cc doctor green across every pristine backup on disk (2.1.216 → 2.1.233); CI (ruff, mypy) passes on Python 3.11–3.14.

Install

uvx patch-cc                 # run without installing
uv tool install patch-cc     # or keep it on PATH
pip install patch-cc         # ordinary PyPI package

Full details: docs/PLAYBOOK.md · docs/INTERNALS.md

patch-cc 0.2.4 — repair the 2.1.227 sentinel collision

Choose a tag to compare

@anfreire anfreire released this 11 Aug 11:01

Repairs patching on Claude 2.1.227. On that build patch-cc refused to do
anything at all — apply and doctor both stopped at

! ... is already patched and no pristine backup exists, so there is
  nothing clean to patch from.

on a freshly installed, untouched binary. Upgrade and re-run patch-cc (or
patch-cc apply --from-cache).

What broke

Nothing in any matcher — every patch is green on 2.1.227. What broke was
detection: is_patched() treated the __cc_ identifier prefix as proof of
patch-cc's own edits, and 2.1.227 ships that prefix itself — __cc_name,
__cc_line, __cc_set, twenty sites inside an env-scrubbing shell snippet.
Every pristine install therefore read as already patched, and the gate that
exists to prevent patching on top of unknown edits blocked the tool's primary
function instead.

Counted across the pristine backups: 2.1.216 through 2.1.226 carry 0
occurrences of __cc_; 2.1.227 carries 20.

The fix

Authorship is declared, never inferred. Every patched bundle ends with the
//patch-cc {...} manifest line — deliberately written as the record of what
was applied — so that record is now the only thing is_patched reads:

def is_patched(source: str) -> bool:
    return read_manifest(source) is not None

Deleted outright: the SENTINEL fingerprint, the legacy --version-marker
fingerprint, and the "patched by an older patch-cc (no manifest)" status
branch that is now an impossible state. Not taken: narrowing the sentinel to
exact identifiers (it already collides — __cc_line is both a patch-cc
arrow-param and upstream's shell variable) or demoting it to a weaker signal
(as of 2.1.227 the prefix is in every pristine build, permanently — a signal
that always fires helps no verdict). Nothing is version-conditional: a
pristine binary is one without our record, on 2.1.227 and every build after
it.

Binaries patched by pre-manifest versions of patch-cc now read as clean;
Claude's daily auto-update replaced that population long ago.

Verified

doctor is green against pristine 2.1.227 and against all nine kept backups
(2.1.216 → 2.1.226), and a real 2.1.227 apply/status/restore round-trip
behaves: backup created, manifest read back, second doctor tests against
the backup. The full postmortem lives in the
release-watch ledger.

patch-cc 0.2.3 — repair the 2.1.226 reducer drift

Choose a tag to compare

@anfreire anfreire released this 09 Aug 23:29

Repairs live-thinking on Claude 2.1.226. On that build the patch was
dropped at bake time, so a patched binary silently lost live thinking. Re-run
patch-cc (or patch-cc apply --from-cache) to get it back.

What broke

2.1.226 threaded a new progress flag through the stream reducer: a second
declarator appended to the head's let (let{...}=t,d=...;) and five arm
bodies rewritten from stmt1,stmt2; into if(stmt1,stmt2,d)call;. The
handler matcher hard-required }=t;, so the reducer was never recognised and
every required step reported nothing — while all four anchor strings sat
healthy in the bundle, which is exactly what the release-watch ledger saw. The
arm rewrites had separately, and silently, killed the message_stop literal
pair.

The fix

The reducer rewrites no longer model anything the minifier owns — the same
doctrine as 0.2.2's ARRAY_CALL repair, applied to control flow:

  • The head is bounded at the grammar's declarator edge ((?=[;,])): after
    let{…}=t only ; or , can follow in a minified bundle, and which one is
    noise.
  • reducer-destructured and reducer-inner collapse into one
    reducer-options step — the options bag carrying onStreamingThinking or
    not is the same shape with the prop present or absent, reused or threaded on
    one code path.
  • Every arm-body literal (~15 enumerated spellings) is gone. Each state update
    is an insertion at the dispatch point: after a case label chain, or
    wrapping the dispatch test itself as (test&&((update),!0))
    value-preserving, so any surrounding composition survives unread. A
    2.1.226-style body reshape is invisible to the patch now.
  • Each dispatch point is its own named step (request-start, message-stop,
    text-clear, message-delta-clear, thinking-start, thinking-append):
    a folded-away arm reads as that point's absence by name, and
    live-thinking's counts are uniform across every supported build —
    deviation is signal, not noise.

The playbook records the rule: rewrite at the dispatch point; never model
the arm.

Housekeeping: the codebase conforms to ruff 0.16's widened default rule set,
and the lockfile picks up the refreshed dev toolchain.

Verification

doctor green against all ten pristine backups, 2.1.216 → 2.1.226, with
uniform counts on every build; the fully patched 2.1.226 bundle passes
node --check; and the rewritten reducer from every build was executed
against a synthetic stream and passes the state-machine checks — clear on
request start, virtual message on block start, delta accumulation, redacted
data, end timestamp on stop.

No new options; upgrading matters if you are on 2.1.226 or later.

Full changelog: v0.2.2...v0.2.3

patch-cc 0.2.2 — repair the 2.1.224 zod-factory drift

Choose a tag to compare

@anfreire anfreire released this 07 Aug 10:04

Repairs max-effort and codex-models on Claude 2.1.224. On that build both
were dropped at bake time, so a patched binary silently lost /effort max
persistence and every registered Codex model. Re-run patch-cc (or
patch-cc apply --from-cache) to get them back.

What broke

2.1.224 re-minified most of the bundle's zod schemas from a per-module namespace
alias (w.enum([...])) to a bare call of the standalone factory (Ir([...])) —
.enum([ drops from 241 sites to 23 in a single release. Two matchers named
that member access and lost their required steps. Because MODEL_ENUM is the one
home discover_models reads, model discovery also fell back to its hardcoded
haiku/sonnet/opus, dropping fable along with every imported Codex id — the
visible tell in doctor's models: line.

The fix

The callee was never what identified those schemas. It is minifier noise that
churns every few releases — E.enum(, A.enum(, b.enum(, v.enum(,
w.enum(, Ir( for one unchanged schema across 2.1.216–224 — so nothing about
it is modelled any more. base.ARRAY_CALL skips from the property name to the
array literal, bounded only by statement and block edges. Enumerating the
spellings seen so far would only have deferred the same break to the next one.

Two things fell out of that:

  • max-effort's schema rewrite now splices the level list into the array it
    found rather than rebuilding the call, so it is idempotent by construction and
    its already-applied branch — provably a no-op on every callee spelling — is
    gone.
  • Dissolving the callee raises what the surviving anchors must carry, per
    site. effortLevel: plus its level array is unique on every build; model:
    plus an array is not (6 sites on 2.1.216–220, 12 on 2.1.221–224), so
    MODEL_ENUM's describe-string tail is its sole discriminator rather than
    redundancy. That is now recorded in the playbook with the method behind it:
    measure an anchor's match count on every backup before removing any part of it.

Verification

doctor green against all eight pristine backups, 2.1.216 → 2.1.224 — both sides
of the migration — plus a real 2.1.224 bake.

No behaviour changes and no new options; upgrading is only worthwhile if you are
on 2.1.224 or later.

Full changelog: v0.2.1...v0.2.2

patch-cc 0.2.1

Choose a tag to compare

@anfreire anfreire released this 26 Jul 23:20

Highlights

/effort max now survives restarts. max has always been first-class in-session — only saving it ran through two whitelists that stopped at xhigh. Both are widened now, so /effort max persists as your default for new sessions exactly like the other levels. It's in the default patch set; nothing to configure.

Hide (or replace) the org/email on the welcome screen. On a personal claude.ai account, the welcome line's third segment is your account email. --org-label hides it — the separator leaves with the segment, emitting upstream's own no-org line — and --org-label "Ada's Lab" shows that instead. Off by default: a bare apply or an untouched menu save never alters it, and /status, the login screen, and org messages keep showing the real account.

uvx patch-cc apply --org-label              # hide the welcome screen's org/email
uvx patch-cc apply --org-label "Ada's Lab"  # ...or show this instead

The menu reads as three groups. Output & display · Models & effort · Chrome & branding — named for what a patch changes for you, not which feature it belongs to. Display-only: patch ids, flags, and the manifest are untouched.

Also in this release

  • A fresh demo gif, and the Codex models + reversibility story surfaced in the README.
  • docs/PLAYBOOK.md carries full matcher entries for both new patches — anchors, failure signals, and why each is safe to lose.

Notes

  • If a Claude update reverts the max-effort patch, a settings file that still says "max" reads as unset on the clean binary — the default effort, never a broken settings parse. Re-bake and the preference is back.
  • doctor verifies both new patches green on every build from 2.1.216 through 2.1.220.

Install / upgrade

uvx patch-cc              # run without installing
uv tool install patch-cc  # or install on PATH
pipx install patch-cc     # or via pipx / pip

Full changelog: v0.2.0...v0.2.1