Skip to content

SessionEnd journal import is cancelled by Claude Code's 1.5 s hook budget; project-scoped scan parses every transcript on the machine #180

Description

@bilersan

Summary

The plugin's SessionEnd hook (ctx journal import --all -y) is cancelled by Claude Code on every exit, so the journal is never imported. Two causes stack:

  1. Claude Code gives SessionEnd hooks a shared 1.5 s budget, and a timeout set on a plugin-provided hook does not raise it.
  2. ctx journal import --all (and ctx journal source) fully parse every transcript under ~/.claude/projects, for all projects, before applying the cwd filter, then spawn one git remote get-url per parsed session. On a machine with a few hundred MB of transcripts this takes tens of seconds, so it can never fit the budget.

Since Claude Code 2.1.271 the cancellation is shown to the user on exit:

SessionEnd hook [command -v ctx >/dev/null 2>&1 || exit 0; ... cd "$CLAUDE_PROJECT_DIR" && ctx journal import --all -y >/dev/null 2>&1 || true] failed: Hook cancelled

Before that it was cancelled silently, which is probably why nobody reported it.

Environment

  • Claude Code 2.1.284, Windows 11, Git Bash as the hook shell
  • ctx dev build from a fork of main; every code reference below was checked against upstream main @ c6628bb and the lines are identical
  • ~/.claude/projects: 1661 .jsonl transcripts, 3.0 GB, 101 project directories
  • Current project's own transcripts: 8 files, 46 MB

Cause 1: SessionEnd budget

From the Claude Code hooks reference (https://code.claude.com/docs/en/hooks#sessionend):

SessionEnd hooks have a default timeout of 1.5 seconds. [...] Per-hook timeout: [...] The overall budget rises automatically to match the highest per-hook timeout in your settings files, up to 60 seconds. [...] Timeouts set on plugin-provided hooks don't raise the budget.

The hook at internal/assets/claude/hooks/hooks.json#L142 sets neither timeout nor async. Even if it set timeout, the plugin rule above makes it a no-op. The only user-side override is CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS, which blocks exit for that long.

Changelog trail: the 1.5 s kill exists since at least 2.1.74, 2.1.268 fixed the env override for hooks without their own timeout, 2.1.271 started showing the hook and its cancellation in the UI.

Cause 2: project-scoped scan parses the whole machine

  • findSessionsWithFilter scans ~/.claude/projects (plus Copilot and Codex dirs) via ScanDirectory, which calls p.ParseFile on every transcript. The cwd filter is applied only after everything is parsed.
  • FindSessionsForCWD then calls gitRemote(s.CWD) for every parsed session. gitRemote has no memoisation, so it is one git subprocess per session even when hundreds of sessions share a cwd. Process spawn is expensive on Windows.

Measurements on the machine above, warm cache (second run identical, so this is CPU and subprocess cost, not disk):

Command Sessions Wall time
ctx journal source --all-projects (parse only, no filter) 420 25.6 s
ctx journal source (cwd filter + gitRemote per session) 6 38.3 s
ctx journal import --all --dry-run 27 new parts 48.7 s

Only 46 MB of the 3 GB parsed belong to the current project. Parse throughput here is roughly 120 MB/s, so the 1.5 s budget is exceeded once a machine has on the order of 150 to 200 MB of transcripts, before any git calls or Markdown rendering.

Consequences

  • .context/journal/ never gets populated on exit. On this machine it did not exist until I ran the dry-run by hand.
  • check-journal nags every prompt with "You have 1661 new session(s) not yet imported". That count comes from CountNewerFiles over all of ~/.claude/projects, so it reports every project's transcripts, not the current project's.
  • ctx journal source takes 38 s for a 6-session project.

Proposed fix

Two independent, small changes:

  1. Detach the import in the hook. Change the SessionEnd command to cd "$CLAUDE_PROJECT_DIR" && nohup ctx journal import --all -y >/dev/null 2>&1 &. The hook returns in milliseconds and is never cancelled. The docs recommend exactly this for work that must outlive the session ("start a fully detached process"). Checked that a nohup'd child survives the hook shell's exit in Git Bash on Windows. Note that plugin hooks.json is what hack/plugin-reload.sh mirrors into the cache, so this is the only file to change on the ctx side. Codex's hooks.json has the same command with "timeout": 3, which has the same problem for the same reason.
  2. Filter before parsing. Peek cwd from the first transcript line (the Claude parser already takes it from the first message, parse.go first.CWD) and apply the cwd predicate before calling ParseFile; memoise gitRemote per cwd. All three matching rules in FindSessionsForCWD use only s.CWD, so behaviour is unchanged and project-scoped scans drop from tens of seconds to well under one. ctx journal source benefits as well.

I am happy to open a PR for either or both.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions