@agents/session-tracker
August 25, 2026 · View on GitHub
Per-agent SessionStart hook + descendant-pid lookup. One state-file format for every coding-agent CLI.
When a coding agent (Claude, Codex, Cursor, Grok, Gemini, Antigravity, …) starts,
each harness fires its own native SessionStart hook with its own payload shape.
This package is a polyglot hook that normalizes all of them into a single
state file, plus the lookup helpers to find "which live session is running in this
terminal / under this pid."
It is the writer side of live-session tracking. The reader that consumes these
files is the VS Code extension (agi-ext src/core/liveSession.ts). This is a
different surface from the CLI's agents sessions command
(cli/src/lib/session/), which parses agent transcript logs, not these state
files — don't conflate the two.
What it writes, where, and when
On each agent's SessionStart event, src/hook.sh drops one JSON file:
~/.agents/.cache/terminals/sessions/<agent-pid>.json
{
"session_id": "…", // parsed per-agent (stdin JSON key, or env var)
"agent": "claude",
"cwd": "/path",
"pid": 12345,
"terminal_id": "…", // from $AGENT_TERMINAL_ID, if set
"launch_id": "…", // from $AGENT_LAUNCH_ID, if set
"ts": 1730000000000,
"method": "hook-stdin" // | hook-env | fs-watch | stdout-banner
}
Writes are atomic (mktemp + mv). Per-agent payload parsing lives in hook.sh:
claude/codex read session_id from stdin JSON; cursor tries
session_id/conversation_id; grok reads $GROK_SESSION_ID from env;
gemini/antigravity try several stdin keys.
Public API
import { trackSpawn, getLiveSession, findStateByPid, descendantPids } from '@agents/session-tracker';
// Poll for the state file a just-spawned agent will drop (default 5s timeout).
const res = await trackSpawn({ shellPid, agent, ... }); // → { confidence: 'high' | 'low', ... }
// Look up the live session for a terminal, by launchId → terminalId → pid tree walk.
const state = await getLiveSession({ launchId, terminalId, shellPid });
Also exported: findStateInTree, findStateByTerminalId, findStateByLaunchId,
pruneStaleSessionState, plus the writer (recordSession, clearSession) and
state-file helpers (serializeState, parseState, writeStateAtomic).
Installing the hook
bun run install-hook claude # register the hook in ~/.claude/settings.json
bun run install-hook claude codex cursor grok
install-hook.ts writes into each harness's native config (Claude
settings.json, Codex/Cursor hooks.json, Grok hooks/session-start.json, Droid
.factory/settings.json, Kimi .kimi-code/config.toml, Hermes .hermes/config.yaml
on_session_start) and is idempotent — prior registrations of this package's
hook.sh are stripped first.
Support is a declarative capability table (HOOK_SUPPORT in src/install-hook.ts),
not a hardcoded switch: every agent either has an installer or returns a specific
reason it can't host the writer hook (gemini is deprecated; antigravity has no
SessionStart event; opencode's SessionStart is a generated plugin, not a shell hook)
— never an opaque "not yet implemented". openclaw/rush have no writable native
SessionStart hook host, so they are out of scope for the writer; their headless rows
still surface via the CLI's discovery comm-map.
Build & test
bun install
bun run build # tsc → dist/
bun test # vitest
bun run test:cold-spawn # 50-iteration reliability test (≥99% match, P95 < 1s)
The files allowlist ships dist/ + src/hook.sh (the hook must be a real file
on disk for harnesses to exec it).