A raw Docker configuration, as `cat ~/.docker/config.json` prints it, keeps its credential under `"auth":`, which only the auth_config name matched. Count `auth` as a credential word when it ends the word, so `"auth":`, `AUTH=` and `oauth=` are redacted while `author` and `auth_method` are not.
Three review findings on the generic credential rule:
- The unbounded name parts around the credential word made the expression rescan the rest of the text after each dot or hyphen. 128,000 characters of `a.` took about 8 seconds, and 256,000 ran past the hook timeout. The name parts are now bounded to 64 characters each, which brings the same input down to about 30 ms.
- A bare value ran to the next whitespace, so `{"token":null,"status":"failed","exit_code":1}` lost its status and exit code. A bare value now ends at the next comma, brace, bracket, semicolon or quote. A JSON object or array value is still redacted to the end.
- A quoted value stopped at an escaped quote, so `{"api_key":"part1\\" part2"}` kept `part2`. Quoted values now skip escaped characters.
An unquoted value ran to the next whitespace, so `DOCKER_AUTH_CONFIG={"auths": {"registry": {"auth": "..."}}}` kept everything after its first space, the auth value included. A value that opens with { or [ is now redacted to the end of the summary.
The generic redaction matched a credential word only right before the separator, so `AWS_SECRET_ACCESS_KEY=...` or `DOCKER_AUTH_CONFIG=...` kept its value in the activity summary. Match a name that contains secret, token, password, api_key, access_key, private_key, credential or auth_config anywhere, and take an unquoted value up to the next whitespace. `max_tokens`, `tokenizer` and `author` stay as they are.
With Claude Code's tool_response read, command output such as `cat .env` now reaches the activity log's output_summary. Its redaction covered a few token forms, not the ones governance-capture detects: generic key, secret and token assignments (`api_key = "..."`, `OPENAI_API_KEY=...`, `"token": "..."`), JWTs, private keys, and ghu_/ghr_ tokens. Redact those too.
Claude Code passes a PostToolUse hook the result of its tool as tool_response. For Bash, that is an object with stdout, stderr, interrupted and isImage. Three hooks read only tool_output, the shape that the Cursor adapter builds, so under Claude Code they never saw any output:
- post-bash-pr-created never printed the PR URL and the review command after `gh pr create`;
- governance-capture never ran its secret detection on command output, such as that of `cat .env`;
- session-activity-tracker logged an empty output_summary for every tool.
Add getToolResult and getToolResultText to hook-input.js. They read tool_response first, then tool_output. Use them in the three hooks. governance-capture now also reads the adapter's { output }, where it used to accept only a string.
Same class of bug as the session_id fix, one field over, found by code-reviewer
before merge: CLAUDE_HOOK_EVENT_NAME is never set for hook subprocesses either
(currently a harmless no-op since the dispatcher only ever calls this hook under
PostToolUse, but a real latent bug if that wiring ever changes). Also runs the
stdin-derived session_id through sanitizeSessionId(), matching the established
pattern already used in cost-tracker.js, ecc-context-monitor.js,
ecc-metrics-bridge.js, and gateguard-fact-force.js.
session-activity-tracker.js is correctly registered in
posttooluse-dispatcher.js's SYNC_HOOKS and fires on every tool call --
but buildActivityRow() only ever read the session id from
ECC_SESSION_ID/CLAUDE_SESSION_ID environment variables, which Claude
Code does not actually set for hook subprocesses. Every row silently
came back null and nothing was ever written to
~/.claude/metrics/tool-usage.jsonl, with no error anywhere.
The session id is available in the hook's own stdin JSON payload
(input.session_id) in real invocations. This prefers that, keeping the
env vars as a fallback. New test added; all 18 existing + new tests
pass.
Also adds a live Activity view to the capabilities dashboard
(scripts/dashboard-web.js) reading that same metrics file, so Skill
vs. Agent vs. raw-tool usage is visible in real time instead of
requiring a manual tail+jq -- verified live against a real session
before this commit. Documents the loopback-only, no-auth design and
the standard SSH-port-forwarding pattern for viewing it from another
device you control.
Fix two lint issues that cause `npm run lint` to exit non-zero:
1. README.md (MD028): Two consecutive blockquotes separated by a bare
blank line. Markdownlint treats this as one blockquote with an
illegal blank line inside. Replace the blank line with a `>`
continuation so both paragraphs stay in the same blockquote.
2. session-activity-tracker.js (eqeqeq): Three instances of `== null`
replaced with explicit `=== null || === undefined` guards to satisfy
the repo's `eqeqeq: warn` ESLint rule.
Closes#1366