Skip to content

🗺 trailhead mod: Claude Code function hooks for on-screen state, context and in-process guards #198

Open
36 of 37 issues completed

Description

@ToRvaLDz

Destination

A released trailhead version whose Claude Code plugin ships a mod (a function-hooks module, Claude Code's early-access plugin API) inside plugins/trailhead, alongside the existing command hooks. On Claude Code it adds: an on-screen view of the work in play (status, pipeline band, live map pane, toasts, compact subagent rows, rendered map/dashboard output, next-command suggestion), active-ticket context that survives compaction, native engine plumbing (model enforcement at subagent spawn, native gh-backed tools, an auto loop driven by turn events), and the guardrails ported in-process. Codex and any host without mods keep working unchanged on the command hooks. Documented in both READMEs and /docs, released by command.

Notes

Parent-repo anchors: conventions #13. Codebase: 🧱 Codebase map.

Branch: this map's work commits straight to main (conventional commits + Refs:), per the conventions' git: main. The former feat/mod integration branch was merged into main and retired on 2026-10-13.

Settled at chart (the fixed frame):

  1. Destination = released, not a local prototype: the mod ships to users inside trailhead.
  2. Packaging = inside the trailhead plugin (one install), not a separate opt-in plugin. This rests on two assumptions verified first by research: a plugin can declare command hooks and a function-hooks module together, and a Claude Code build without mods ignores the module without breaking the plugin.
  3. All four areas are in scope: on-screen UI, context and compaction, native engine plumbing, in-process guards.
  4. Command hooks stay the floor. Codex has no mods, so the core must remain self-sufficient on command hooks; the mod adds or replaces behaviour only where it is loaded. The API is early access, so reversible work (UI, context) lands before the guards.
  5. Develop and try locally with hot reload (--plugin-dir / the session mods folder), claude plugin validate and claude plugin test.

Glossary

  • mod: a function-hooks module (hooks/hooks.json -> modules, exporting register(on)), Claude Code only. Avoid: "function hook plugin" as a separate product.
  • command hook: the existing node scripts registered under hooks in hooks.json (PreToolUse / PostToolUse / SessionStart); the host-neutral floor.
  • band: the row drawn above the prompt (AbovePrompt). pane: a docked side view (Pane). status: the status-line entry ($.ui.status).
  • work state: what the session is doing in trailhead terms (active map, ticket, step); the single fact the UI and context features read.

Decisions so far

  • Can the core plugin ship a mod alongside its command hooks?: yes, one hooks/hooks.json holds hooks + modules; builds without mods skip only the module; Claude Code 2.1.287+. Correction to chart decision 2: the npm installer needs its own mod plugin dir under <configDir>/skills/ (see fog 13).
  • Where does the mod read the work state from?: the skills write .trailhead/work-state.json (map, ticket, title, step, auto, session, updatedAt) through a template helper; the mod reads it on events, observations only flag staleness; a different session marks it orphaned (shown dimmed, never auto-deleted, no TTL); session-ticket unchanged. Implemented by Write the work state from the skills.
  • How do in-process guards coexist with the command-hook guards?: per guard (a guard moves to the mod only once ported and tested); the mod signals via TRAILHEAD_MOD_GUARDS=<guards>@<epoch-ms> refreshed on every tool.call, command hooks skip only listed guards with a fresh stamp; every ported guard fails closed via .catch; search-guard rewrites only safe cd <abs> && <read> shapes, denies the rest. Speed win split out as Merge the Bash guards into one hook process.
  • Scaffold the mod in the plugin with its dev and test loop: hooks/mod/register.ts declared under modules beside the command hooks, $.state contract trailhead.workState in types/index.d.ts, no visible behaviour; npm test now runs every suite plus claude plugin validate / claude plugin test; dev loop in hooks/mod/README.md. Installer and Codex unchanged (pinned by tests).
  • Write the work state from the skills: template helper trailhead-work-state.js (set/clear) writes .trailhead/work-state.json atomically with whole-state validation and the session stamp; the skills call it at work-start (after workspace setup), each step, pause/resume, auto, and clear at Resolve; research excluded, UAT follow-ups map to verify; Codex carries helper and calls (pinned by tests).
  • Status entry and pipeline band showing the work state: the mod draws a status entry (map #n · #t title · step, engine-prefixed ⚠ trailhead:) and an AbovePrompt band (pipeline with the current step, auto · n done, dim when orphaned), refreshed on session/turn events and after a helper call; the helper gained autoDone (--auto-done); module now register.tsx, view logic in work-state-view.ts; verified live on the terminal.
  • Inject the work state into the system prompt: a prompt.compose hook appends a session-scoped trailhead:work-state section (map, ticket with quoted title and link, step), latched in the promptSnapshot atom and rebuilt only on a ticket change, at session start and after a real main-loop compaction, so step changes keep the prompt cache; absent when orphaned or --bare, capped at 1024 bytes; verified headless across a /compact.
  • Merge the Bash guards into one hook process: hooks/trailhead-bash-guards.js runs commit, secret, install, search, body in one process (first block wins, per-guard failure isolation, same verdicts), the only Bash PreToolUse registration on Claude Code and Codex (Codex --guards=commit,secret,install,body), 7 -> 3 spawns per Bash call; each guard exports evaluate(cmd, { env }); a fresh TRAILHEAD_MOD_GUARDS=<names>@<epoch-ms> (5 s, MOD_GUARDS_FRESH_MS) skips exactly those guards, ready for the in-process ports (fog 10).
  • Live map pane listing the active map's tickets: hooks/mod/map-pane.tsx draws a Pane of the active map's open tickets (frontier / in progress / blocked, holders, in-play marker, unverified only counted) from gh via $.process.run, only while open, 15 s floor + one trailing refresh, stale results dropped, last list kept on failure; per-row work button runs /trailhead:work <n> via $.command.run; toggled by /trailhead:panel (a commands/panel.md the mod answers, since mod-registered commands cannot carry trailhead:; left out of Codex), auto-opens once in fullscreen; FORCE_HYPERLINK=1 for terminals Claude Code cannot identify.
  • Burn-down view of the active map's progress: a Burn-down section inside the Map pane (not a pane or band of its own): open tickets over time rebuilt from the map's issues' createdAt/closedAt via one more gh issue list --state all (limit 5000, a dim capped line beyond), superseded/out-of-scope closes counted as removed scope; terminal draws a 3-row Raster, other surfaces a text sparkline; a failed read keeps the last curve (same map only) labelled with its age; verified live on map 198.
  • Switch maps from the panel by clicking one: the Map pane lists every open map (Maps · n, shown only with more than one; active marked ●), a click only views another map (pane-local viewedMap atom, viewing #n · active #m + back to active), the pane never writes .trailhead/active-map; the switch happens on use, since a named map ticket makes its map active (rule in _shared/multi-map.md, pointed to by work and quick); with no active map the open maps are listed to view; verified live (guided UAT).
  • Keep the map panel through /clear and clear before work: the band and the Map pane re-read their state on classic.SessionStart (source clear), the engine's new-session event that fires after /clear has reset the mod's atoms (verified live on 2.1.296, firmer than a timer); the pane carries the auto-open flag and the viewed map across through module variables; the work button runs /clear then /trailhead:work <n> via $.command.run, skipping the verb when the session id did not change (a denied clear resolves, it does not reject); verified live (guided UAT).
  • Add a close button to the map panel footer: the Map pane footer holds refresh + status on the left and close at the right edge (space-between over the pane width); close closes the pane (its ui.close hook stops the timers) and marks the auto-open done, like a close by the person; a status too long beside the buttons (footerStatus in map-pane-view.ts) moves whole to a row of its own under them, so a gh failure keeps its hint; verified live (guided UAT).
  • Add mockup screenshots of the new panels to the docs and website: the canvas map-pane and burn-down artboards were brought up to the shipped panel (maps list, viewing, footer close) and approved; 2x crops (map-panel, map-panel-viewing, burndown, work-state-band) in assets/screenshots/ (README EN/IT) and site/src/assets/screenshots/ (Astro-optimised); README section On screen in Claude Code, /trailhead:panel docs text updated to the shipped panel, new landing section Live in Claude Code after Know where you stand; pre-existing 375px landing overflow split out as Landing scrolls sideways at phone width (map 98).
  • Panel: mark this session's ticket and show paused tickets: new trailhead:paused label (pause adds after the PAUSED checkpoint, resume removes after the claim check, a work/quick claim drops it; nineteen labels at first-use setup); the panel lists paused tickets only under Paused · n (holder, no work), map and dashboard views; a per-session ownTicket atom keeps ▸ + this session on the session's own row even when another session overwrites the shared work-state file; verified by guided UAT. Other surfaces still count a released pause as Frontier: #263.
  • Panel: show open/total ticket counts per map and the whiteboard's assigned tickets: maps list shows N open / M per map from one GraphQL subIssuesSummary read (name-only fallback on failure); dashboard whiteboard heading shows open and assigned counts.
  • Panel: a Whiteboard row above the Maps list, with its counts, in place of the header link: the header dashboard button became a Whiteboard row above the Maps list (map, loading and no-active-map views; not drawn in the whiteboard view, whose map button goes back) showing N open / M from two aliased totalCount connections in the one maps GraphQL call (parseMapsRead, MapPaneData.whiteboard); a failed or unreadable read draws it name-only, never stale counts; verified by guided UAT.
  • Treat paused tickets the same on every surface: trailhead:paused is off the frontier on every surface (-label:trailhead:paused in every frontier query, rule in substrate.md); map/whiteboard renders and the dashboard show a Paused group with the panel's precedence (unverified > paused > blocked > in progress > frontier); taken back only by /trailhead:resume or a named work/quick, auto via its resume step.
  • Map panel loses its repo when the session cwd moves: hooks/mod/repo-pin.ts pins the session checkout (toplevel + a remote gh can serve, Enterprise via gh auth status); every pane gh runs with cwd = pinned root, pane and band read .trailhead/* under it; a cwd outside any repo keeps the pin, a move to another served repo re-pins and resets the pane; never pinned → "Not in a GitHub repository", no gh call; uat.setup: [] for this repo; work button and prompt links left to Panel work button and prompt links still follow the session cwd #269.
  • Panel work button and prompt links still follow the session cwd: the work button refuses (short toast) unless the session is in the pinned repo; prompt links use the pin's host and owner/name; remoteId is the single remote parser.
    Work-state prompt section missing when the work state is set mid-session (Work-state prompt section missing when the work state is set mid-session #271): the engine keeps the first composed system prompt, so a mid-session work-state change reaches the model as prompt.submit context once per change (stale note when cleared); only the first compose after a reset writes the latch.
    Work-state band hidden when the ticket runs in an isolated worktree: the mod finds this session's work state in the repo's other checkouts (worktrees, sibling <repo>-t<n> clones) when the own checkout has none or another session's
  • Refresh the map panel when a ticket is opened or closed: open-ticket lists read through GraphQL (not the lagging search index), one follow-up read after a write within the floor, wider write matcher
    Cancel a queued ticket from the map panel (Cancel a queued ticket from the map panel #304): a panel press during a running turn is held by the panel (nothing sent to the host) and runs at turn end; cancel drops it, conversation untouched.
  • Panel says no GitHub repository after /trailhead:new created one mid-session: the host's session.repo() stays null for a repo created mid-session (Claude Code 2.1.306), so currentRepo($) in repo-pin.ts falls back to git in the cwd (rev-parse --show-toplevel, then remote get-url origin) when it answers null; the pin refresh (pane and band) and the work-press check share it; a host read that throws stays "checkout unreadable"; verified live in tmux.
  • Panel work press moves into the pinned repo instead of refusing: outside the pinned repo a work/resume press sends the verb with a trailing --in=<base64url(root)>; the work cluster moves there first through templates/trailhead-checkout-in.js (validates a checkout root, POSIX-quoted lone cd, post-move here required, fails closed) before the load and the Preflight; no pin, an unreadable or removed checkout still refuse before the /clear; quick out of scope.
  • Map panel does not follow the whiteboard ticket in play or a newly created one: the panel switches to the whiteboard once per own whiteboard ticket in play and stays there after Resolve; a whiteboard ticket created from a map raises a toast hint, no switch.
  • Show the total open ticket count across all maps and the whiteboard: the Map pane draws a dim N open across all maps + whiteboard line (under the Whiteboard row, and the Dashboard header) from one aliased GraphQL totalCount query over the dashboard's label scopes (trailhead:map-<n> per open map + trailhead:whiteboard); at least N when the maps read is cut; a failed count keeps the last total only over the same maps; /trailhead:map states the same total via the dashboard's gh issue list counts and limits.
  • Map panel keeps a closed ticket as held and its dependents blocked: bug fixed. A dropped trailing refresh timer left the pane on the pre-close list; the trailing timer is now tracked and replaced, and a turn end after a tracker write always reads.
  • Map panel drops a read in flight when the pin moves within the same repo (Map panel drops a read in flight when the pin moves within the same repo #315): a same-repo pin move no longer drops the read in flight; it is read again (generation bumped only for another repo, plus a pin-move counter in the stale check).
  • Map panel fails with no-surface on a fresh session (Map panel fails with no-surface on a fresh session #321): with no surface attached yet the panel command keeps the pane pending and answers a plain message, never closes it or leaks the SDK reason.
  • 🐛 Map panel says "gh not installed" when the followed checkout was deleted: a deleted followed checkout now re-pins to a live checkout of the same repo, else reports checkout not found: <path>.
    Map pane does not refresh after a multi-line Bash tracker write: not reproducible; measured, every write starts a read within 0.3s or at the next trailing read inside the 15s floor. Regression tests added, no mod change.
  • Map pane write matcher counts a heredoc body line as a tracker write: the pane's write check strips heredoc bodies before the regex, through an import-free leaf held equal to shell-scan.js by a parity test

Not yet specified

  1. Ticket-closed feedback: toast + optional $.audio.speak when a ticket resolves or verify passes; opt-in surface (config key?) open.
  2. Compact subagent rows: ui.render on UserMessage task notifications for trailhead-* agents (name, step, duration), full row on ctrl+o.
  3. Rendered map / dashboard output: /trailhead:map and /trailhead:dashboard drawn as Markdown with clickable ticket links (onLinkPress).
  4. Next-command suggestion: prompt.suggest proposing /trailhead:work <n> after a resolve; must respect the /clear-first handoff rule.
  5. Checkpoint before compaction: session.compact triggers a pause checkpoint of the ticket in play; what it writes (a PAUSED comment vs a local note) and when it is too noisy open.
  6. Model enforcement at spawn: agent.spawn sets the model for trailhead-* agents from config.models.* (and the model-defaults.json tiers) instead of relying on the prose dispatch.
  7. Native gh-backed tools: $.tool.register tools (read ticket, frontier, map) returning typed data, fewer Bash calls and permission prompts; which tools, and how the skills prefer them only when present.
  8. auto loop on turn events: moved to 🗺 trailhead auto as fog.
  9. Guards in-process: port the guards one by one to tool.call / tool.check and the injection scanner to session.append, per the frame settled in How do in-process guards coexist with the command-hook guards? #201 (per-guard replacement, stamped env signal, fail-closed .catch, safe-shape rewrite); builds on Merge the Bash guards into one hook process #207. Open: porting order and the stamp threshold.
  10. Bash statusline fate: whether ~/.claude/statusline-trailhead.sh is retired, kept for the terminal, or reads the same work state.
  11. Release and docs: both READMEs, /docs, badges (hook counts change), CHANGELOG, a minimum Claude Code version note (2.1.287, tested build pinned), the Codex projection verified to skip the module, and a mod-only off switch via a userConfig boolean read in register.
  12. npm delivery of the mod: bin/trailhead.js places a dedicated mod plugin dir under <configDir>/skills/ (outside plugins/trailhead/skills), plus uninstall, install-verify, and a dedupe when both the plugin and the npm copy load a mod.

Out of scope

  • A mod for Codex or any other host (no mod API there).

Activity

  1. ToRvaLDz commented on Oct 12, 2026

    @ToRvaLDz
    OwnerAuthor

    Autonomous run summary (2026-10-12)

    • Why it stopped: no auto-workable ticket. The map has no open tickets; no paused ticket to resume.
    • Resolved this run: none.
    • Remains (needs the human): all 13 items in Not yet specified are fog and must be dissolved into tickets before a run can work them (burn-down view, ticket-closed feedback, compact subagent rows, rendered map/dashboard, next-command suggestion, checkpoint before compaction, model enforcement at spawn, native gh-backed tools, auto loop on turn events, guards in-process, Bash statusline fate, release and docs, npm delivery of the mod).
    • Blocked dependents: none.
  2. 9 remaining items

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    trailhead:mapthe trailhead map (index issue)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions