Skip to content

feat: compute cch via xxHash64 of full request body#192

Open
Arkptz wants to merge 7 commits into
griffinmartin:mainfrom
Arkptz:feat/cch-xxhash64
Open

feat: compute cch via xxHash64 of full request body#192
Arkptz wants to merge 7 commits into
griffinmartin:mainfrom
Arkptz:feat/cch-xxhash64

Conversation

@Arkptz

@Arkptz Arkptz commented Apr 14, 2026

Copy link
Copy Markdown

Summary

Replace SHA-256-based cch computation with xxHash64 of the full serialized request body, matching Claude Code's actual signing mechanism.

The previous implementation used SHA-256(firstUserMessageText)[:5], but analysis of Claude Code's custom Bun binary reveals the actual algorithm:

  1. Billing header is built with a cch=00000 placeholder
  2. The full request body is serialized to JSON
  3. xxHash64(bodyBytes, seed) & 0xFFFFF → 5-char hex
  4. The placeholder is replaced with the computed hash

The seed (0x6E52736AC806831E) was extracted from Claude Code's Bun runtime and verified against the Python reference implementation across all input lengths. It has remained constant across versions (v2.1.37 → v2.1.97).

Also includes:

  • Pure TypeScript xxHash64 implementation (zero dependencies)
  • ccVersion bump from 2.1.90 to 2.1.97
  • Removal of dead computeCch() function and its export
  • Developer scripts for cch verification (extract-cch) and prompt testing (test-prompt)

Related issue

Related to #188, #190 — correct cch computation is a prerequisite for reliable billing validation.

Testing

  • All existing tests pass (assertions updated for new cch behavior)
  • New xxHash64 test suite: spec vectors, determinism, seed consistency (8 tests)
  • xxHash64 output verified against Python reference implementation
  • extract-cch.ts intercept proxy confirms our hash matches real Claude CLI output
  • make all passes locally

Checklist

  • PR title follows Conventional Commits (feat:, fix:, docs:, chore:, etc.)
  • make all passes locally (runs lint, build, and test)
  • Tests added or updated where applicable
  • README or docs updated where applicable

@griffinmartin griffinmartin left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful reverse-engineering work — the xxHash64 implementation is clean, the seed-extraction story is convincing, and tests pass. Before merge, a few things must be addressed.

Must-fix

1. Rebase onto current main — branch is stale

  • mergeStateStatus: DIRTY, mergeable: CONFLICTING.
  • Conflict source is src/transforms.ts: main now uses a prefixName(...) helper (PascalCase tool prefixing, PR #197) at lines 11/231/248; this PR still uses ${TOOL_PREFIX}${tool.name} directly.
  • Releases 1.5.0 → 1.5.3 have shipped since this PR was opened.

2. Update ccVersion to current

main is at ccVersion: "2.1.112" (src/model-config.ts:13). This PR proposes 2.1.97, which is already two CC releases behind. Bump to whatever ships at merge time.

3. Re-verify CCH_SEED against Claude Code 2.1.112

The PR description states the seed is constant across v2.1.37 → v2.1.97. main is now 2.1.112. Please re-run scripts/extract-cch.ts against the current Claude Code release and confirm 0x6E52736AC806831En still produces matching hashes before merge. Reverse-engineered constants tied to a vendor binary need re-confirmation on every version bump.

4. Harden the placeholder replacement (src/transforms.ts:248)

const final = serialized.replace("cch=00000", `cch=${cch}`)

Two real failure modes:

  • String.prototype.replace(string, …) only replaces the first occurrence. If user-supplied content (a prompt, a moved system entry, a tool result) ever contains the literal cch=00000, the wrong location is patched and the request fails validation while the log still records a plausible-looking cch.
  • If the placeholder is missing entirely (e.g. an upstream caller bypasses buildBillingHeaderValue, or a future transform strips the billing header), replace is a silent no-op. The body goes out without a real cch but log("transform_cch", { cch, … }) still reports a value — false confidence.

Suggested fix: anchor on the trailing ; (cch=00000;) which is structurally part of the billing header format, and assert exactly one replacement occurred. Pseudo:

const PLACEHOLDER = "cch=00000;"
const idx = serialized.indexOf(PLACEHOLDER)
if (idx === -1 || serialized.indexOf(PLACEHOLDER, idx + 1) !== -1) {
  log("transform_cch_placeholder_missing_or_duplicate", { ... })
  return serialized // or throw, depending on policy
}
const final = serialized.slice(0, idx) + `cch=${cch};` + serialized.slice(idx + PLACEHOLDER.length)

5. Decide on the computeCch export removal

This package is published (opencode-claude-auth@1.5.3, dist/index.d.ts). computeCch is exported from src/index.ts:51 on main and removed in this PR. Two acceptable resolutions:

  • (preferred) Restore as a thin back-compat shim in signing.ts/index.tscomputeCch is three lines, costs nothing, and external consumers may exist.
  • Otherwise, relabel the commit as feat!: with a BREAKING CHANGE: footer per Conventional Commits, and let release-please bump the major.

The current feat: title with a silent public-API removal is the wrong combination.

Should-fix

6. Add a regression vector pinning CCH_SEED

src/xxhash64.test.ts covers algorithm correctness against the empty/seed-0 spec vector and computeCchHash's format/determinism — but nothing pins (body, expected_cch) against CCH_SEED. If the seed is ever changed by accident, the only thing that catches it is the manual scripts/extract-cch.ts (which requires the Claude CLI installed).

Add at least one fixture, e.g. capture a known body with the placeholder and assert the resulting cch matches a hard-coded value.

7. Strengthen xxhash64 spec coverage

The empty-input/seed-0 vector exercises only the avalanche + length-add paths. Add at least one of the standard xxHash test corpus vectors that cover:

  • short non-empty input (<8 bytes)
  • input crossing the 32-byte stripe boundary

This protects future maintainers against subtle off-by-one or stripe-loop regressions.

8. Mark cch as the last mutation in transformBody

At the end of transformBody (src/transforms.ts ~line 244):

// IMPORTANT: cch must be computed last. Any mutation of `parsed` or
// `serialized` after this point desyncs the hash from the body that
// will be sent on the wire.

Cheap insurance; the function is long enough that someone will eventually want to add a step at the bottom.

Nits (non-blocking)

  • buildBillingHeaderValue now produces a template (cch=00000;) rather than a final header. The doc comment you added is sufficient; if you're already touching the file, a Template suffix in the name would make it clearer at call sites, but optional.
  • scripts/extract-cch.ts uses hardcoded /tmp/... paths and has a benign race between setTimeout(..., 3000) and clearTimeout(timer) on child close. Dev-only script, not worth blocking on.

Summary of required actions

  1. Rebase onto main (resolve transforms.ts conflict against prefixName).
  2. Bump ccVersion to current.
  3. Re-run scripts/extract-cch.ts against Claude Code 2.1.112 and confirm seed validity in a comment on this PR.
  4. Anchor and validate the cch placeholder replacement; fail loudly if missing or duplicated.
  5. Restore computeCch export OR relabel commit as feat!:.
  6. Add at least one fixture-based cch regression vector.
  7. Add at least one short-input xxhash spec vector.
  8. Add the "must remain last" comment near the cch step.

The core change is good and matches my read of the Bun binary's behavior. Once items 1–5 land I'm happy to approve.

@griffinmartin

Copy link
Copy Markdown
Owner

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the old SHA-256-based cch computation (first 5 hex chars of SHA-256 of the first user message) with an xxHash64-of-the-full-request-body approach that matches the actual algorithm used in Claude Code's Bun binary. It also bumps ccVersion to 2.1.97 and adds two developer scripts for verification.

  • New xxhash64.ts: pure TypeScript BigInt xxHash64 implementation with the extracted seed (CCH_SEED = 0x6e52736ac806831e); computeCchHash hashes the full serialized body (with placeholder in place) and returns a 5-char hex of the low 20 bits.
  • transforms.ts change: after all body mutations are applied and the body is serialized, the cch=00000 placeholder is replaced via String.replace with the computed hash; buildBillingHeaderValue now always emits the placeholder instead of a pre-computed hash.
  • Test suite: existing assertions updated to check format rather than specific known hash values; the xxHash64 spec vector for empty input with seed 0 is verified.

Confidence Score: 3/5

The core xxHash64 implementation is correct and the signing refactor is clean, but the string-replacement step in transforms.ts has a position-dependent correctness issue that warrants a fix before merging.

The serialized.replace("cch=00000", ...) call replaces the first occurrence in the serialized JSON. If a client sends messages before system in the JSON body and user content contains the literal substring cch=00000, the replacement lands in the user message rather than the billing header — leaving system[0] with the placeholder and silently mutating user content. This is a present defect on an untested code path. Additionally, computeCchHash tests assert only format and self-consistency, so a wrong seed would pass all tests without external validation being wired into CI.

Files Needing Attention: src/transforms.ts (the replace call at line 250) and src/xxhash64.test.ts (missing a pinned expected value for computeCchHash)

Important Files Changed

Filename Overview
src/transforms.ts Adds xxHash64-based cch computation after full body serialization; uses string.replace() with a plain string that targets only the first occurrence, which may hit user message content before the billing header if JSON property order puts messages first
src/xxhash64.ts New pure-TypeScript xxHash64 implementation using BigInt; primes, accumulator lanes, merge, and avalanche steps match the spec; verified against the known empty-input spec vector
src/xxhash64.test.ts Tests verify spec vector, determinism, and format of computeCchHash, but no test pins a known expected output for the CCH_SEED hash value, leaving seed correctness unverifiable from the test suite alone
src/signing.ts Removes computeCch (SHA-256 path) and makes buildBillingHeaderValue always emit cch=00000 placeholder; clean and correct change
src/transforms.test.ts Updated cch assertions from specific hash values to format-only checks (non-placeholder 5-char hex); weaker regression coverage but tests still fail if placeholder is not replaced
src/model-config.ts Bumps ccVersion from 2.1.90 to 2.1.97; straightforward version string update

Sequence Diagram

sequenceDiagram
    participant Client
    participant transformBody
    participant buildBillingHeaderValue
    participant computeCchHash
    participant API as Anthropic API

    Client->>transformBody: POST body (JSON string)
    transformBody->>buildBillingHeaderValue: messages, version, entrypoint
    buildBillingHeaderValue-->>transformBody: "billing header with cch=00000 placeholder"
    transformBody->>transformBody: inject into system[0], relocate non-core entries
    transformBody->>transformBody: JSON.stringify parsed object
    transformBody->>computeCchHash: TextEncoder bytes of serialized JSON
    computeCchHash->>computeCchHash: xxhash64(bytes, CCH_SEED) AND 0xFFFFF
    computeCchHash-->>transformBody: 5-char hex cch value
    transformBody->>transformBody: "replace first cch=00000 with computed cch"
    transformBody-->>Client: final body with real cch
    Client->>API: POST with computed cch in billing header
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/transforms.ts:250
**String replacement targets first occurrence regardless of position**

`String.prototype.replace` with a plain string argument replaces only the *first* occurrence of `"cch=00000"` in the serialized JSON. JSON property order follows insertion order, so if the original request body serializes `messages` before `system` (e.g., the client sent them in that order), and a user message contains the literal text `"cch=00000"`, the replacement hits the user-message content instead of the billing header — leaving `system[0].text` with the placeholder and silently mutating the user's message.

A targeted replacement that anchors to the billing header prefix avoids the ordering dependency, e.g. anchoring to the billing header key before replacing the value.

No test currently exercises a request where `messages` precedes `system` in JSON order and user content contains `"cch=00000"`.

### Issue 2 of 3
src/xxhash64.test.ts:40-74
**No known-value end-to-end test vector for `computeCchHash`**

The `xxhash64` tests include the spec vector for empty input with seed 0, which validates correctness of the core hash function. However, all `computeCchHash` tests only assert format (`/^[0-9a-f]{5}$/`), determinism, or self-consistency (computing via both paths with the same `CCH_SEED`). None assert against a *known expected hash value* from the actual Claude CLI intercept. If `CCH_SEED` contains a transcription error, all these tests still pass — the format check only confirms a 5-char hex string is produced. Adding one test that pins a specific `computeCchHash` output (from the `extract-cch.ts` probe results or Python reference) would close this gap.

### Issue 3 of 3
src/xxhash64.ts:123-124
The comment says "v2.1.37" but the PR description notes the seed was verified across v2.1.37 → v2.1.97 and the version is being bumped to 2.1.97. Updating the comment to reflect the verified range makes it easier to know when re-verification is needed.

```suggestion
/** The seed baked into Claude Code's custom Bun binary (verified v2.1.37–v2.1.97) */
export const CCH_SEED = 0x6e52736ac806831en
```

Reviews (1): Last reviewed commit: "style: fix formatting" | Re-trigger Greptile

Comment thread src/transforms.ts
const serialized = JSON.stringify(parsed)
const encoder = new TextEncoder()
const cch = computeCchHash(encoder.encode(serialized))
const final = serialized.replace("cch=00000", `cch=${cch}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 String replacement targets first occurrence regardless of position

String.prototype.replace with a plain string argument replaces only the first occurrence of "cch=00000" in the serialized JSON. JSON property order follows insertion order, so if the original request body serializes messages before system (e.g., the client sent them in that order), and a user message contains the literal text "cch=00000", the replacement hits the user-message content instead of the billing header — leaving system[0].text with the placeholder and silently mutating the user's message.

A targeted replacement that anchors to the billing header prefix avoids the ordering dependency, e.g. anchoring to the billing header key before replacing the value.

No test currently exercises a request where messages precedes system in JSON order and user content contains "cch=00000".

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transforms.ts
Line: 250

Comment:
**String replacement targets first occurrence regardless of position**

`String.prototype.replace` with a plain string argument replaces only the *first* occurrence of `"cch=00000"` in the serialized JSON. JSON property order follows insertion order, so if the original request body serializes `messages` before `system` (e.g., the client sent them in that order), and a user message contains the literal text `"cch=00000"`, the replacement hits the user-message content instead of the billing header — leaving `system[0].text` with the placeholder and silently mutating the user's message.

A targeted replacement that anchors to the billing header prefix avoids the ordering dependency, e.g. anchoring to the billing header key before replacing the value.

No test currently exercises a request where `messages` precedes `system` in JSON order and user content contains `"cch=00000"`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread src/xxhash64.test.ts
Comment on lines +40 to +74
describe("computeCchHash", () => {
it("returns a 5-char lowercase hex string", () => {
const body = new TextEncoder().encode('{"prompt":"hello","cch":"00000"}')
const result = computeCchHash(body)
assert.match(result, /^[0-9a-f]{5}$/, "must be 5-char hex")
})

it("is deterministic (same input produces same output)", () => {
const body = new TextEncoder().encode('{"prompt":"hello","cch":"00000"}')
const r1 = computeCchHash(body)
const r2 = computeCchHash(body)
assert.equal(r1, r2, "same input must produce same hash")
})

it("uses CCH_SEED consistently", () => {
const body = new TextEncoder().encode('{"model":"claude","cch":"00000"}')
const fromComputeCch = computeCchHash(body)
// Manually compute using xxhash64 with CCH_SEED
const hash = xxhash64(body, CCH_SEED)
const expected = (hash & 0xfffffn).toString(16).padStart(5, "0")
assert.equal(
fromComputeCch,
expected,
"computeCchHash must use CCH_SEED internally",
)
})

it("produces different hashes for different inputs", () => {
const body1 = new TextEncoder().encode('{"a":1,"cch":"00000"}')
const body2 = new TextEncoder().encode('{"b":2,"cch":"00000"}')
const r1 = computeCchHash(body1)
const r2 = computeCchHash(body2)
assert.notEqual(r1, r2, "different inputs should produce different hashes")
})
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No known-value end-to-end test vector for computeCchHash

The xxhash64 tests include the spec vector for empty input with seed 0, which validates correctness of the core hash function. However, all computeCchHash tests only assert format (/^[0-9a-f]{5}$/), determinism, or self-consistency (computing via both paths with the same CCH_SEED). None assert against a known expected hash value from the actual Claude CLI intercept. If CCH_SEED contains a transcription error, all these tests still pass — the format check only confirms a 5-char hex string is produced. Adding one test that pins a specific computeCchHash output (from the extract-cch.ts probe results or Python reference) would close this gap.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/xxhash64.test.ts
Line: 40-74

Comment:
**No known-value end-to-end test vector for `computeCchHash`**

The `xxhash64` tests include the spec vector for empty input with seed 0, which validates correctness of the core hash function. However, all `computeCchHash` tests only assert format (`/^[0-9a-f]{5}$/`), determinism, or self-consistency (computing via both paths with the same `CCH_SEED`). None assert against a *known expected hash value* from the actual Claude CLI intercept. If `CCH_SEED` contains a transcription error, all these tests still pass — the format check only confirms a 5-char hex string is produced. Adding one test that pins a specific `computeCchHash` output (from the `extract-cch.ts` probe results or Python reference) would close this gap.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread src/xxhash64.ts
Comment on lines +123 to +124
/** The seed baked into Claude Code's custom Bun binary (v2.1.37) */
export const CCH_SEED = 0x6e52736ac806831en

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The comment says "v2.1.37" but the PR description notes the seed was verified across v2.1.37 → v2.1.97 and the version is being bumped to 2.1.97. Updating the comment to reflect the verified range makes it easier to know when re-verification is needed.

Suggested change
/** The seed baked into Claude Code's custom Bun binary (v2.1.37) */
export const CCH_SEED = 0x6e52736ac806831en
/** The seed baked into Claude Code's custom Bun binary (verified v2.1.37–v2.1.97) */
export const CCH_SEED = 0x6e52736ac806831en
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/xxhash64.ts
Line: 123-124

Comment:
The comment says "v2.1.37" but the PR description notes the seed was verified across v2.1.37 → v2.1.97 and the version is being bumped to 2.1.97. Updating the comment to reflect the verified range makes it easier to know when re-verification is needed.

```suggestion
/** The seed baked into Claude Code's custom Bun binary (verified v2.1.37–v2.1.97) */
export const CCH_SEED = 0x6e52736ac806831en
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants