Skip to content

v0.5.0 - P1 capture-fidelity batch (#5, #6, #7)#14

Merged
jsirish merged 6 commits into
mainfrom
claude/p1-capture-fidelity
Jul 6, 2026
Merged

v0.5.0 - P1 capture-fidelity batch (#5, #6, #7)#14
jsirish merged 6 commits into
mainfrom
claude/p1-capture-fidelity

Conversation

@jsirish

@jsirish jsirish commented Jul 5, 2026

Copy link
Copy Markdown
Member

The three P1 findings from the v0.4.0 audit (docs/AUDIT-v0.4.0.md), shipped as one v0.5.0 batch. The buffer now records why (user intent), captures a much wider slice of what a session does, and stamps compaction seams idempotently. A fifth hook joins the set.

Changes

Shared refactor

The redaction/cleaning jq defs and the _tl_err breadcrumb moved into hooks/_lib.sh (tl_jq_redact_defs, tl_err, tl_append_line, clamp) so the capture-side hooks can't drift - same reasoning as tl_resolve_sid. The command-path redact stays byte-identical (proven by the unchanged pre-existing suite).

Pre-merge review history (5 rounds)

This PR went through five rounds of high-effort automated review before merge. The short version: round 1 added a prose-safe redact_prompt (separate from the command-tuned redact, which corrupts/inverts ordinary English) plus assorted fixes. Rounds 2 and 3 each tried to fix a keyword-boundary regex in redact_prompt and each fix was correct for its target case but broke a different real case (substring matching -> word-boundary -> missed SCREAMING_SNAKE_CASE compounds -> excluded common real phrasing like "secrets:" entirely). That's the same failure shape as a documented precedent in this repo's own history (the v0.2.0 redaction rework, see CHANGELOG). Per that precedent, round 4 stopped patching the regex and did a structural redesign instead:

  • redact_prompt's generic keyword+separator rule was removed entirely. There is no boundary rule that admits "secrets:"/"passwords:" while excluding "author:"/"tokens:" - the shapes overlap. redact_prompt now masks ONLY unambiguous structural signals: known token prefixes, PEM, URL-userinfo, and length-gated Bearer/Token/Basic scheme values. A colon-form secret with no recognizable prefix/scheme is now a deliberate, consistent, documented gap - backstopped by the handoff skill's human re-scan, same class as redact's existing bare-CLI-flag gap - rather than being redacted inconsistently depending on suffix.
  • Added the Token-scheme rule redact_prompt was missing entirely (an oversight from splitting it out of redact).

Round 5 confirmed the redesign held - zero findings against redact_prompt itself - and found two smaller, unrelated bugs: session-onboard.sh leaked a shell diagnostic to stderr on an unreadable buffer file (an unguarded integer test on grep -c's empty-string output), and the mcp__* fallback branch embedded a raw tool name directly inside the **...** bold-marker pair without stripping asterisks, which could desync onboard's classifier regex. Both fixed. Three lower-priority cleanup items (duplicated timestamp format string, redundant onboard greps, duplicated auth-scheme regex literals) were filed as issue #16 rather than fixed under continued time pressure.

Full commit-by-commit detail: 1f2f7b2 (round 1 + initial fixes), 0737a0f (round 2), 02ca0ed (round 3), 6f2976c (round 4 redesign), bdaa367 (round 5).

Also filed: issue #15 (pre-existing, out-of-scope cosmetic quote-handling bug in redact's command path).

Deferred: concurrent-fire precompact non-atomicity (compaction isn't concurrent with itself; flush has the same pre-existing pattern).

Verification

  • shellcheck -s sh hooks/*.sh tests/*.sh clean
  • JSON manifest checks pass
  • sh tests/run.sh -> 136 passed / 0 failed (+41 new)
  • Every fix across all five rounds stress-tested against the reviewer's exact cases plus additional edge cases beyond the test suite (plurals, scheme words as ordinary nouns, quote interaction, unreadable files)

Closes #5, closes #6, closes #7

🤖 Generated with Claude Code

jsirish and others added 6 commits July 5, 2026 16:49
The P1 findings from the v0.4.0 audit. The buffer now records why (user
intent), captures a wider slice of what a session does, and stamps
compaction seams idempotently. A fifth hook joins the set.

- #5: UserPromptSubmit -> session-prompt.sh captures redacted, truncated
  user prompts. Intent previously lived only in the compactable conversation.
- #6: PostToolUse matcher widened to Grep/WebFetch/WebSearch/Task/Agent/mcp__.*;
  Read/Glob deliberately excluded. MCP tools captured name-only.
- #7: precompact stamp made idempotent (tail -n 1 guard), mirroring flush.

Shared refactor: redact/clean defs and the _tl_err breadcrumb moved into
_lib.sh (tl_jq_redact_defs, tl_err, tl_append_line, clamp) so the two
capture-side hooks can't drift. Command redact stays byte-identical.

Pre-merge high-effort review fixes:
- Prose-safe redact_prompt: the command-tuned redact corrupts/inverts English
  ("password is not the problem" -> "password is ***"). Prompts now use a
  restricted path (structural token formats + explicit key:value/key=value
  secrets only); command path unchanged.
- Onboard no longer counts prompt-only buffers as unconsumed (a session with
  no captured action has nothing to distill), preserving the warning's signal.
- Prompt clamped to 2000 chars before redaction (multi-MB paste latency).
- Task empty-string description falls through to prompt (jq // only skips null).

Tests: 95 -> 121 assertions. shellcheck + JSON manifest checks green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKPWvcm32Acg7UXJF2gQp
… gaps

A second high-effort review of the round-1 fixes found the fixes themselves
introduced new bugs:

- onboard's prompt-only-buffer skip used a substring search for "**prompt**"
  anywhere in a line, so a real action line whose own content mentioned that
  literal string (a grep for the pattern, routine in this repo) made the
  whole buffer look prompt-only and silently dropped it from the unconsumed
  warning. Fixed by anchoring to the type-marker position instead.
- redact_prompt's keyword rule matched substrings ("auth" inside "author",
  "token" inside "tokens"), corrupting prose like "author: rewrite the intro"
  in the exact path introduced to prevent that. Fixed with \b word boundaries.
- redact_prompt dropped the Bearer/Basic scheme rules entirely, leaking a
  pasted Authorization header verbatim. Restored via a shared _auth_scheme
  def used by both redact paths (shape-constrained, low prose false-positive
  risk unlike the bare-token-word rule that stays excluded).
- WebSearch query and Task/Agent description are prose but were still routed
  through the command-tuned redact, reproducing the same corruption issue #5
  fixed for prompts ("fix token refresh bug" -> "fix Token *** bug"). Switched
  both to redact_prompt; Grep (regex) and WebFetch (URL) correctly stay on
  redact.

Tests: 121 -> 128 assertions, all fixes reproduced and verified before and
after. shellcheck + JSON manifest checks green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKPWvcm32Acg7UXJF2gQp
A third high-effort review of round 2's fixes found each one had introduced
a new bug of its own:

- redact_prompt's \b word-boundary (round 2's fix for substring matching)
  silently stopped matching SCREAMING_SNAKE_CASE compounds (CLIENT_SECRET,
  ACCESS_TOKEN, DB_PASSWORD) since underscore is a \w char. Real secrets in
  the standard naming convention passed through completely unredacted. Fixed
  with a letter-only lookaround boundary instead of \b: underscore still
  counts as a boundary, an adjacent letter still doesn't.
- Onboard's prompt-only filter used a grep-into-grep pipe whose "found
  nothing" and "found nothing because there's nothing to find" are
  indistinguishable, so a buffer with zero conforming lines (corrupted, or a
  capture hook that failed every call) silently fell through the same
  continue as a genuine prompt-only buffer - regressing the guarantee that
  any existing ended buffer gets surfaced. Fixed with an explicit
  total-vs-prompt-count comparison.
- The shared _auth_scheme def (round 2's fix for the Bearer/Basic leak) was
  unconstrained enough to corrupt ordinary prose ("basic authentication
  support" -> "Basic *** support"), reopening the exact corruption class
  redact_prompt exists to prevent. Split into a command variant (unchanged)
  and a new _auth_scheme_prose with a 16+ char length floor.

Tests: 128 -> 134 assertions. Also stress-tested additional edge cases not
covered by tests (start-of-string keywords, "credentialing" prefix-match,
no-separator prose) to reduce the odds of a round 4. shellcheck + JSON
manifest checks green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKPWvcm32Acg7UXJF2gQp
A fourth review found four bugs, two in the same keyword-boundary code round
3 had just patched: the letter-lookaround fix excluded not just the intended
false-positive cases but EVERY keyword followed by any letter, so common real
phrasing ("secrets:", "passwords:", "credentials:") silently stopped being
redacted at all. Three consecutive rounds of "correct for the target case,
breaks a different one" on the same regex - the same shape as the v0.2.0
redaction rework documented earlier in this repo's history. Per that
precedent, stopped patching and did a structural redesign instead of a fifth
regex tweak (user confirmed: drop the generic rule).

- redact_prompt's generic keyword+separator rule removed entirely. There is
  no boundary rule that admits "secrets:"/"passwords:" while excluding
  "author:"/"tokens:" - the shapes overlap. redact_prompt now masks ONLY
  unambiguous structural signals: known token prefixes, PEM, URL-userinfo,
  and length-gated Bearer/Token/Basic scheme values. A colon-form secret with
  no recognizable prefix/scheme is now a deliberate, CONSISTENT, documented
  gap backstopped by the handoff skill's human re-scan - not fixed
  differently case-by-case.
- Added the missing Token-scheme rule to _auth_scheme_prose (redact_prompt
  never had one - an oversight from splitting it out of redact in round 2).
- The 16-char length floor un-redacting short real Bearer/Basic values is
  accepted as the same documented gap, rather than tuning the threshold
  further.
- Noted (not fixed, pre-existing, out of scope): a cosmetic quote-handling
  bug in redact's generic rule (command path, unchanged) - an orphaned
  trailing quote, no secret leak. No longer applies to redact_prompt since
  that rule is gone.

Tests: 134 -> 133 (net change reflects removing tests for behavior that no
longer exists and consolidating documented-gap assertions). Stress-tested
broadly beyond the reviewer's cases (plurals, scheme words as ordinary nouns,
quote interaction) before resubmitting. shellcheck + JSON manifest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKPWvcm32Acg7UXJF2gQp
A fifth review confirmed the round-4 structural redesign is clean (zero
findings against redact_prompt - the three-round regex cycle is over) and
found two smaller, unrelated bugs:

- session-onboard.sh's prompt-only counters leaked a shell diagnostic to
  stderr on an unreadable buffer file. grep -c prints an empty string, not
  "0", when it cannot read the file at all, and the integer test was not
  guarded - the one error path in this hook not already 2>/dev/null'd. Fixed
  by defaulting both counts to 0 when empty.
- The mcp__* fallback branch embedded the raw tool name directly inside the
  **...** bold-marker pair without stripping asterisks (every other branch
  only puts field content after a fixed literal marker). An unusual tool
  name containing * would break the markdown span and desync onboard's
  classifier regex, silently miscounting a real action as prompt-only. Fixed
  by stripping asterisks from the tool name specifically.
- Bash branch's hand-rolled truncation migrated to the shared clamp def.

Filed issue #16 for three deferred cleanup items (duplicated timestamp
format, redundant onboard greps, duplicated auth-scheme regex literals) -
none are correctness bugs, deferred to a calmer follow-up rather than more
mechanical edits under today's time pressure.

Tests: 133 -> 136 assertions. shellcheck + JSON manifest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKPWvcm32Acg7UXJF2gQp
tests/run.sh redefined the prompt() test helper twice inline (once to inject
THROUGHLINE_DISABLE=1, once back to the original) to test the kill-switch
path. CI's shellcheck (apt-installed on ubuntu-latest) flagged every call to
prompt() between the definitions as SC2218 "This function is only defined
later" - a redefinition pattern local shellcheck did not flag, caught only
once the GitHub Actions run itself failed.

Replaced the redefine-and-restore pattern with a direct inline pipe for the
one disabled-state test, so prompt() is defined exactly once. Verified by
running apt-get install shellcheck (matching CI's install method) against
the fix in a fresh Ubuntu container - clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKPWvcm32Acg7UXJF2gQp
@jsirish jsirish merged commit 95bab64 into main Jul 6, 2026
1 check passed
@jsirish jsirish deleted the claude/p1-capture-fidelity branch July 6, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant