Skip to content

fix(session): keep a tool call's arguments masked when masking leaves no JSON to parse - #1482

Merged
SaulMoro merged 1 commit into
Tencent:mainfrom
hz89Dev:fix/scrub-unparsable-tool-arguments
Oct 28, 2026
Merged

SaulMoro merged 1 commit into
Tencent:mainfrom
hz89Dev:fix/scrub-unparsable-tool-arguments

Conversation

@hz89Dev

@hz89Dev hz89Dev commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A scrubbed session can carry a token unmasked in a tool call. When masking replaces a number in the call's arguments, such as a declared secret WAREHOUSE_PIN=4938 in { "pin": 4938, "token": "ghp_…" }, the masked arguments are no longer JSON. Scrubbing then kept the original arguments, so the token next to the number reached the archive. It now keeps the masked text as { "redacted": "<masked arguments>" }.

scrubBlock(tool_call)
  masked = mask(JSON.stringify(arguments))
  parses  -> arguments = parsed masked JSON          (as before)
  fails   -> before: arguments = original arguments  (secrets included)
             now:    arguments = { redacted: masked }

This reaches every scrubbed archive: score --drain, the background pass, session push --session, workflow publish --sessions, and session push / session migrate --push with --scrub. #1478 made declared secrets, which can be 4 characters, part of that masking, so a numeric value is now a likely trigger. An environment secret with a numeric value could already trigger it.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npx tsc --noEmit, npm run lint: clean.
  • npx vitest run on scrub-session, session-title, score-drain, session-push, session-cmd, agent-session-find: 168 passed. The new scrub-session test fails without the fix, with the token back in the arguments.
  • Real CLI, before and after. A git-kind team on a local bare origin declares WAREHOUSE_PIN in env/secrets.yaml. A captured unattended Claude Code session has a tool_use with input {"pin":4938,"token":"ghp_A1b2…"}, and CI=true WAREHOUSE_PIN=4938 teamai score --drain archives it.

Built from main (993984d):

$ CI=true WAREHOUSE_PIN=4938 teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✔ Published 1 label to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git grep -c ghp_ teamai-reports
teamai-reports:sessions/_unattributed/tester/claude-code_open-the-vault_20261027.jsonl:1

Built from this branch:

$ CI=true WAREHOUSE_PIN=4938 teamai score --drain
✔ Judged 1 sampled session here (session-quality)
✔ Published 1 label to teamai-reports
✔ Archived 1 captured session to teamai-reports (scrubbed)
✔ Nothing left to do here
rc=0
$ git grep -c ghp_ teamai-reports
(no match)
$ git grep -h -o "redacted[^}]*}" teamai-reports -- "sessions/*.jsonl"
redacted":"{\"pin\":<REDACTED:WAREHOUSE_PIN>,\"token\":\"<REDACTED:gh_tok>\"}

Not run here, left to CI: the gitlab and github providers, Windows, and agents other than Claude Code.

Related Issues

For #1385. Follow-up to #1478, found while reviewing it.

Notes for Reviewers

  • Masking each value on its own instead would keep the structure but lose the key context one pattern needs ("password": "…"), so the masked text is kept whole.

@github-actions

Copy link
Copy Markdown

No findings in the specified diff.

The fallback preserves the masked arguments instead of restoring secrets, and the regression test covers numeric-secret masking that makes the JSON unparsable.

The PR description documents sufficient testing: type checking, lint, 168 targeted tests, and a representative before/after real-CLI score --drain run. The additional provider and agent coverage explicitly deferred to CI is not blocking.

@github-actions

Copy link
Copy Markdown

No findings in the specified diff. The earlier review’s no-findings conclusion still holds.

The fallback retains masked arguments rather than restoring secrets, and the regression test covers numeric masking that makes the JSON invalid.

The PR description documents sufficient testing, including a representative before/after real-CLI score --drain run. Extra provider and agent coverage deferred to CI is not blocking.

Review was read-only; no PR code was run, built, or installed.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

@hz89Dev 9d30c88 declares sessions as an install key, while your #1483 makes it a member setting. We would rather take #1483: a member who writes sessions: { publish: { unattended: off } } in ~/.teamai/config.yaml expects the opt-out to reach their workspaces, and as an install key a workspace ignores it and publishes under the auto default. Could you drop 9d30c88 here? #1483 fixes main's red types.test.ts on its own, and we will merge it first; this PR is approved on the scrub fix once its CI is green on top of it.

@SaulMoro

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

@hz89Dev #1483 is merged, so main is green again. Please drop 9d30c88 and rebase this onto main. After that, CI on the new head is all this needs before we approve.

@hz89Dev
hz89Dev force-pushed the fix/scrub-unparsable-tool-arguments branch from 9d30c88 to 1e863ff Compare October 27, 2026 23:20
@github-actions

Copy link
Copy Markdown

No findings in the specified diff. The earlier reviews’ no-findings conclusion still holds.

The fallback preserves masked arguments instead of restoring secrets, and the regression test covers numeric-secret masking that makes the JSON invalid.

The PR description documents sufficient testing, including a representative before/after real-CLI score --drain run. Extra provider and agent coverage explicitly deferred to CI is not blocking.

Review was read-only; no PR code was run, built, or installed.

… no JSON to parse

A placeholder that replaces a number, such as a declared secret 4938,
leaves the masked arguments unparsable, and scrubbing then kept the
original arguments, a token beside it included. It now keeps the masked
text.
@hz89Dev
hz89Dev force-pushed the fix/scrub-unparsable-tool-arguments branch from 1e863ff to 3ad59c8 Compare October 27, 2026 23:50
@github-actions

Copy link
Copy Markdown

No findings in the specified diff. The earlier reviews’ no-findings conclusion still holds.

The fallback preserves masked arguments instead of restoring secrets, and the regression test covers numeric-secret masking that makes the JSON invalid.

The PR description documents sufficient testing, including a representative before/after real-CLI score --drain run. Additional provider and agent coverage explicitly deferred to CI is not blocking.

Review was read-only; no PR code was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This was generated by AI during triage.

Approving at 3ad59c8, which is aae5fa7 rebased onto main with the sessions commit dropped (range-diff identical).

Built CLI, main against this PR (git-kind team declaring WAREHOUSE_PIN=4938): after CI=true teamai score --drain and teamai session push --session … --source claude-code, the ghp_… token next to the pin appears twice in the team repo's history on main and not at all here.

  • A numeric secret at the top level, nested in an object or in an array now comes out as {"redacted": "…"} with the masked text; a secret inside a string keeps the arguments' structure, as before; non-secret numbers (12345, 4939, 49380) are unchanged.
  • session pull, list, search and resume into Claude Code and Codex all handle {redacted} arguments. No dashboard code reads archived tool-call arguments.
  • scrub.ts:74 is the only place that parses JSON after masking, so no other spot has this fallback.
  • npx tsc --noEmit and npm run lint exit 0; the 6 related unit files (168 tests) pass, also with a symlinked TMPDIR; the new test fails on main because the original token comes back. CI is green at this head, on a merge with main after #1483.

[P3 nit] The comment at scrub.ts:76 says a placeholder in "a key" breaks parsing; keys are always quoted, so only a bare number or literal triggers the fallback.

Two related gaps we found while checking, both on main already, not caused by this PR:

  • A declared secret containing ", \ or a control character is never masked in tool-call arguments: masking runs on the JSON text, where those characters are escaped, so the secret no longer matches (pa"ss1234 came through unmasked).
  • redactJson (workflow records, the JSON envelope) does not mask a number equal to a declared secret: {"pin":4938} is kept.

@hz89Dev could you take those two in a follow-up PR?

@SaulMoro
SaulMoro merged commit 12c34ed into Tencent:main Oct 28, 2026
44 of 46 checks passed
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.

3 participants