From cfca5c81535d208d6922bf7e8aaed7cc67bdeaed Mon Sep 17 00:00:00 2001 From: Clovis Date: Sat, 5 Sep 2026 16:58:17 -0700 Subject: [PATCH] fix: session-activity-tracker never records (CLAUDE_SESSION_ID unset) 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. --- scripts/dashboard-web.js | 9 ++++++++ scripts/hooks/session-activity-tracker.js | 8 ++++++- tests/hooks/session-activity-tracker.test.js | 22 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/dashboard-web.js b/scripts/dashboard-web.js index e504fe7e3..1ad612510 100644 --- a/scripts/dashboard-web.js +++ b/scripts/dashboard-web.js @@ -6,6 +6,15 @@ * Usage: node scripts/dashboard-web.js [port] * Open http://localhost:3456 * + * The server only binds to loopback and has no authentication, by design -- + * it is meant to be viewed on the same machine it runs on. To check it from + * another device you control (a tablet, a laptop), use standard SSH local + * port forwarding instead of exposing the port itself: + * ssh -L 3456:127.0.0.1:3456 @ -p + * then open http://127.0.0.1:3456 on the *remote* device. This requires + * only SSH access you already control on your own machine -- nothing here + * opens the dashboard to the network. + * * Contribution: https://github.com/affaan-m/ECC */ diff --git a/scripts/hooks/session-activity-tracker.js b/scripts/hooks/session-activity-tracker.js index 43e5b7afe..f6edc39f5 100644 --- a/scripts/hooks/session-activity-tracker.js +++ b/scripts/hooks/session-activity-tracker.js @@ -567,7 +567,13 @@ function buildActivityRow(input, env = process.env) { } const toolName = String(input?.tool_name || '').trim(); - const sessionId = String(env.ECC_SESSION_ID || env.CLAUDE_SESSION_ID || '').trim(); + // Claude Code does not set CLAUDE_SESSION_ID as an environment variable for + // hook subprocesses -- the session id is only available in the hook's own + // stdin JSON payload. Prefer that; keep the env vars as a fallback for any + // caller that does set them. + const sessionId = String( + input?.session_id || env.ECC_SESSION_ID || env.CLAUDE_SESSION_ID || '' + ).trim(); if (!toolName || !sessionId) { return null; } diff --git a/tests/hooks/session-activity-tracker.test.js b/tests/hooks/session-activity-tracker.test.js index 8819c750d..bd541c454 100644 --- a/tests/hooks/session-activity-tracker.test.js +++ b/tests/hooks/session-activity-tracker.test.js @@ -407,6 +407,28 @@ function runTests() { fs.rmSync(tmpHome, { recursive: true, force: true }); }) ? passed++ : failed++); + (test('reads session_id from stdin JSON when no session env var is set', () => { + // Claude Code does not set CLAUDE_SESSION_ID for hook subprocesses in + // real invocations -- the session id only arrives via stdin JSON. + const tmpHome = makeTempDir(); + const input = { + session_id: 'stdin-session-only', + tool_name: 'Read', + tool_input: { file_path: path.join(tmpHome, 'foo.txt') }, + }; + const result = runScript(input, { + ...withTempHome(tmpHome), + CLAUDE_HOOK_EVENT_NAME: 'PostToolUse', + }); + assert.strictEqual(result.code, 0); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'tool-usage.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.session_id, 'stdin-session-only'); + + fs.rmSync(tmpHome, { recursive: true, force: true }); + }) ? passed++ : failed++); + (test('handles invalid JSON gracefully', () => { const tmpHome = makeTempDir(); const invalidInput = 'not valid json {{{';