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.
This commit is contained in:
Clovis
2026-09-05 16:58:17 -07:00
parent ec74a88644
commit cfca5c8153
3 changed files with 38 additions and 1 deletions
+9
View File
@@ -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 <user>@<this-machine> -p <ssh-port>
* 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
*/
+7 -1
View File
@@ -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;
}
@@ -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 {{{';