Skip to content

fix(store): record cwd for live Claude Code 2.1.283 proxy traffic - #640

Merged
lis186 merged 1 commit into
mainfrom
fix/live-proxy-cwd-pr
Sep 27, 2026
Merged

lis186 merged 1 commit into
mainfrom
fix/live-proxy-cwd-pr

Conversation

@lis186

@lis186 lis186 commented Sep 27, 2026

Copy link
Copy Markdown
Owner

摘要

修正即時 proxy 流量的 Claude cwd 仍然是 null 的問題。

Details

  • Root cause: live Claude Code 2.1.283 bodies carry a top-level system (starting with x-anthropic-billing-header: cc_version=2.1.283…, about 27 KB) that has no Primary working directory: line. The env block moved to a role: 'system' message. store.extractCwd's system branch returned null without looking further.
  • Why fix: subagent import, Claude/Codex proxy cwd, 1h cache pricing, and indexed effort #637 missed it: stored _req.json keeps sysHash instead of system, so the stored-body analysis behind c4f293f took the context_management branch and looked correct. After re-attaching shared/sys_<hash>.json and shared/tools_<hash>.json to the 22 stored requests from the re-verification run, 0/22 yielded a cwd; stored bodies alone gave 20/22.
  • Fix (server/store.js): when system has no cwd line, requests carrying context_management continue to the role-system messages, then messages[0], then safeguards[].classifier_context.live_cwd.
    • Role-system messages are checked before messages[0], because 2.1.283's messages[0] embeds CLAUDE.md <system-reminder> text that may quote an unrelated Primary working directory: line. The security scan flagged this ordering (F1).
    • Requests without context_management (title generation, subagent kickoff) still return null as before, so isAnthropicSubagent / isLikelySubagent classification is unchanged.
  • Title generation inherits the session's known cwd through the existing sessionMeta path, and stays isSubagent: true.
  • Tests: a new test/live-proxy-cwd.e2e.test.js drives a real proxy against a mock upstream with a 2.1.283-shaped main turn and a same-session title request. It checks the index cwd, the title request's isSubagent and cwd inheritance, and sessions.json after shutdown. Unit cases were added to test/store.test.js.
  • Docs: docs/wire-protocol-reference.md gains a changelog entry and a corrected Claude cwd detection description.

Verification

  • CCXRAY_HOME=$(mktemp -d) CCXRAY_EXPORT_DISABLE=1 npm test: 2645/2646. The one failure, hub-owned config status divergence ("row 2: local agent-port refusal reaches stdout", timed out), is unrelated. It fails identically with the pre-fix server/store.js on this machine and passes in CI on main (fix(cli): print --help and reject unknown options before booting #639). The real ~/.ccxray was untouched during the run.
  • Fail-on-old / pass-on-new: test/store.test.js plus test/live-proxy-cwd.e2e.test.js against the pre-fix store.js fail 5 of 72, all actual: null; with the fix, 72/72 pass.
  • Live run: Claude Code 2.1.283 claude -p --effort low with a general-purpose subagent, through an isolated proxy. All 5 entries and sessions.json recorded the working directory. Subagent agentKey/isSubagent were unchanged from the pre-fix run.
  • Independent review (agentflow security-scan + acceptance, Claude-family reviewers): Outcome, Minimality and Conformance all PASS. Security findings F1 and F6 are folded into this commit.

Not verified:

  • Claude Code versions other than 2.1.283.
  • Deferred security findings, not in this PR:
    • F2: a pre-existing dashboard onclick DOM XSS on project names (public/miller-columns.js:1858). It is easier to reach now that cwd is recorded, and it is the next fix in the queue.
    • F3: export repoRoot() without sanitizeName().
    • F4/F5: cwd control characters and validation.

🤖 Generated with Claude Code

Claude Code 2.1.283 still sends a top-level `system` on context_management
requests, but the env block with "Primary working directory:" moved to a
role:'system' message. extractCwd returned null as soon as `system` lacked
the env line, so the live proxy recorded cwd: null on every Claude entry and
session. #637 was validated against _req.json files, which store `system`
only as sysHash, so the saved bodies took the context_management branch and
looked fixed.

extractCwd now falls through to the existing messages / safeguards scan when
a context_management request's `system` has no env line. A `system` that
names the directory still wins. role:'system' messages are read before
messages[0], because on 2.1.283 messages[0] embeds CLAUDE.md and other
<system-reminder> text that can quote an env line of its own.

Subagent classification is unchanged: without context_management a
cwd-less `system` still yields null, so title-generation requests and
subagent kickoffs keep "no cwd"; isAnthropicSubagent and isLikelySubagent
already return early on context_management. A title-generation entry takes
the session's already-known cwd, as before.

New e2e test drives the real proxy path with a mock upstream and checks
index.ndjson (main turn cwd, title-gen isSubagent and inherited cwd) and
sessions.json after shutdown. Old code: 0/3 e2e and the new unit tests fail;
new code: all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lis186
lis186 merged commit 53558f1 into main Sep 27, 2026
3 checks passed
lis186 added a commit that referenced this pull request Sep 27, 2026
…lers (#641)

The project row and the star badge built their onclick source with
JSON.stringify(value).replace(/"/g, '&quot;'). JSON.stringify does not escape
`&`, and the browser decodes the attribute before compiling it as JS, so a
literal `&quot;` in the data became a real quote and escaped the string: a
directory named `p&quot;);alert(1);(&quot;` ran code when its project row or
star was clicked. Session ids are just as exposed, because metadata.session_id
is accepted verbatim. #640 made the path easier to reach by recording cwd for
live Claude traffic again.

Both values now travel in data-* attributes (escapeHtml'd), and the handlers
are fixed strings that read this.dataset, following the existing data-sid /
data-resume convention. keyboard-nav.js read project names back by parsing
the onclick source; it now reads data-project, which also fixes arrow-key
navigation skipping a project whose name JSON.parse could not recover.

An audit of every inline on*= handler under public/ found no other site that
splices a data-controlled string: the rest use constants, numeric indexes,
ccxray-generated entry ids, hex hashes, server-derived agent keys, or the
data-* pattern already. There are no javascript: URLs.

New puppeteer e2e (test/dashboard-xss-e2e.test.js) clicks the project row,
the project star, the session star and the derived-star chip for a payload
name and session id, and checks keyboard navigation. Old code: 5 of 6 fail;
new code: all pass.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
lis186 added a commit that referenced this pull request Sep 27, 2026
The re-verification showed transcript-only reporting misses about 12% of an
interactive host (prompt suggestions and title generation never reach the
transcript), so the MVP now reads merged proxy + import data and labels each
Ask complete / partial proxy coverage / transcripts only. Records that proxy
index lines and their imported twins are pruned after 14 days, splits A1 into
a terminal-only A1a and a persistence A1b behind a pricing prerequisite, and
updates the #640 cwd gap. Revised after a threeways review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lis186 added a commit that referenced this pull request Sep 28, 2026
The re-verification showed transcript-only reporting misses about 12% of an
interactive host (prompt suggestions and title generation never reach the
transcript), so the MVP now reads merged proxy + import data and labels each
Ask complete / partial proxy coverage / transcripts only. Records that proxy
index lines and their imported twins are pruned after 14 days, splits A1 into
a terminal-only A1a and a persistence A1b behind a pricing prerequisite, and
updates the #640 cwd gap. Revised after a threeways review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lis186 added a commit that referenced this pull request Sep 28, 2026
* docs: ccxray × agentflow integration spec (draft)

Transcript-first per-Ask usage reports for agentflow notebooks (phase A,
no agentflow change), and two tool-neutral upstream proposals (phase B:
worker run ledger, close-time usage reporter). Records the 2026-09-26
evidence behind PR #637.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agentflow): dashboard hides imported turns by design; adapter reads the index

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agentflow): revise spec after adversarial review (GPT-6 Astra, Fable 5.1)

- Ask windows: Reply stamp is the end (close commit only a cross-check);
  start is the host user message matching the Ask body; empty scaffolds,
  idle gaps, overlaps, retried closes and reopened rounds defined
- External workers join host-authored dispatch records first; clone path
  plus time bound; B1 becomes 'make the dispatch record a contract'
- Codex has no responseId: one source per Codex session until a tested key
- Reports default to the ccxray data dir; --beside uses the git common dir
  exclude and is refused in stream worktrees (agentflow cleanup rejects
  unknown ignored files)
- Claude transcripts default to 30-day retention: snapshots move into A1;
  settled is decided from on-disk data with source fingerprints
- B2 block is writer-owned like the stamp, generated once, reused on retry
- Corrected stamp format, close semantics, archive naming, workspace-dir;
  added privacy rules and background-agent rows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agentflow): record re-verification on merged main e1b882c

- Interactive hosts: ~12% proxy-only traffic (prompt suggestions, titles);
  transcript-only host rows are lower bounds and labelled as such
- Claude proxy+import merge verified at read time; index holds both rows,
  hideImported is presence-based; Codex double-counting verified
- Dispatch join verified 18/18; match on recorded cwd, effort from the
  top-level field, dedupe usage by message.id
- Ask start by first bullet, window tail after the Reply stamp, setup row,
  host session from the matched message
- New A1 prerequisites: live proxy cwd still null on CC 2.1.283, pricing
  differs across proxy/import/reload paths, prompt-suggestion executor kind

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agentflow): put the proxy in the MVP; split A1 into A1a/A1b

The re-verification showed transcript-only reporting misses about 12% of an
interactive host (prompt suggestions and title generation never reach the
transcript), so the MVP now reads merged proxy + import data and labels each
Ask complete / partial proxy coverage / transcripts only. Records that proxy
index lines and their imported twins are pruned after 14 days, splits A1 into
a terminal-only A1a and a persistence A1b behind a pricing prerequisite, and
updates the #640 cwd gap. Revised after a threeways review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(agentflow): record owner decisions on completeness and A1a acceptance

Three completeness labels and the per-turn-twin rule for complete are
decided; A1a does not require exact executor time. A-001..A-008 never went
through the proxy, so A1a acceptance expects transcripts only for all of
them and the complete/partial paths need a later proxied Ask or fixtures;
listed as a known gap. Notes that runner truncation of the captured report
does not affect the dispatch fields attribution uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant