Repository navigation
0.14.0
Why
On Claude Code, trailhead now draws the work on screen while you work instead of only in the transcript: which ticket is in play and at what step, the active map's tickets with a button to work one, and how the map is burning down. It arrives as a mod (a Claude Code function-hooks module) that ships inside the plugin next to the command hooks, so the command hooks stay the floor on Codex and on builds without mods. This release also hardens the shell guards against many ways of hiding a secret, a secret read or an empty body from them.
Added
- The trailhead mod (Claude Code 2.1.287+, plugin install) (#202).
hooks/mod/register.tsx, declared undermodulesinhooks/hooks.jsonbeside the command hooks; a Claude Code build without mods skips only the module. Typed state contract intypes/index.d.ts;npm testnow runs every suite plusclaude plugin validate/claude plugin test. The npm installer and the Codex projection do not ship it yet: on those installs nothing changes. - Work state file (#205). The skills write
.trailhead/work-state.json(map, ticket, title, step, auto run, session) throughtemplates/trailhead-work-state.jsat work-start, at every step, on pause/resume and inauto, and clear it at Resolve. It is gitignored local state; the mod only reads it, and a file from another session shows as orphaned (dimmed, never deleted). - Status entry and pipeline band (#203). A
trailhead:status entry (map #n · #t title · step) under the prompt and a band above it with the ticket and thediscuss › plan › execute › verifypipeline (auto · n doneduring an autonomous run,‖ paused, dim when orphaned). - Work state in the system prompt (#204). A short
trailhead:work-statesection (map, ticket with its link, step) is added to the system prompt at session start and after a compaction, rebuilt only on a ticket change so step changes keep the prompt cache; absent when orphaned. - Live map panel,
/trailhead:panel(#208). A pane listing the active map's open tickets as Frontier / In progress / Blocked, with holders, the ticket in play marked▸, and aworkbutton per frontier ticket. It reads the tracker throughghonly while open (15 s floor), keeps the last list with a dim reason whenghfails, opens by itself once in fullscreen, and/trailhead:paneltoggles it. - Burn-down in the map panel (#228). Open tickets over time from the map's first ticket to now (
open · done · removed · peak; superseded and out-of-scope closes count as removed scope): a 3-row chart on the terminal, a sparkline on the desktop app; a failed read keeps the last curve with its age. - Switch maps from the panel (#229). With more than one open map, a
Maps · nlist (active map marked●); a click only views another map (viewing #n · active #m,back to active). The active map changes on use: working a named map ticket makes its map the active one (rule in_shared/multi-map.md). - Panel through
/clear, clear before work (#230). The band and the panel restore their state after/clear; theworkbutton runs/clearand then/trailhead:work <n>, so each ticket starts on a clean context. - Close button in the panel footer (#231).
refreshand the status on the left,closeon the right; a status too long beside the buttons moves to a row of its own.closecounts as closing it by hand (no auto-open again that session). - Screenshots of the panels (#232). README (EN/IT), the
/trailhead:paneldocs page and a new Live in Claude Code section of the website landing show the map panel, map switching, the burn-down and the band, taken from the approved mockups.
Changed
- Secret, body and secret-read guards fail closed on constructs they cannot interpret (#227).
eval,source/., a bash 5.3 function substitution (${ cmd; },${| cmd; }),printf -v, a function definition, an alias, agh apibody field written with a quoted or split key (-f 'body'=,-f bo""dy=) and a backslash-newline inside a word used to be emulated case by case; the gaps (echo ${ gh issue comment ... },alias g=gh; g issue comment ...,source ./post.shbefore a write, a quoted api key in the body guard) let a write through unchecked. They are now a closed list the guards do not emulate: a final gate, after every existing verdict, blocks withSECRET_GUARD_UNMODELLED_CONSTRUCT,BODY_GUARD_UNMODELLED_CONSTRUCTorSECRET_READ_UNMODELLED_CONSTRUCTand a message asking for the simple form (its sample is truncated and redacted). A hit blocks only when it can matter to that guard: the code it hides or the function / alias it defines (when called) holds a write, a write that can depend on them followssource/printf -v(it has an expansion, a~or a relative path; a fully literal write passes), its own statement is a write, or (read guard) a secret-looking piece sits next to it;alias ll='ls -l',source ~/.venv/bin/activate && pytest,eval "$(ssh-agent)"and the same words inside quoted prose or a comment pass. Supersedes #221 and #224 (individual cases of the same gap). The shipped hook libs now includeunmodelled.js. - The five Bash guards run in one hook process (#207). The commit, secret, install, search, and body guards used to be five
PreToolUse(Bash)registrations, so every Bash call started five node processes. They now share one,trailhead-bash-guards.js, which evaluates them in the same order with unchanged verdicts: the first block wins, a guard that errors allows on its own without stopping the others, and a guard module that cannot load is skipped with a stderr warning and a session advisory naming it. The guardrails themselves are unchanged (still the same set of distinct protections); only the registrations and process spawns shrink, to three hook processes per Bash call on Claude Code and Codex. On Codex the hook is registered with--guards=commit,secret,install,body(no search guard). A freshTRAILHEAD_MOD_GUARDS=<name,...>@<epoch-ms>stamp, set by the mod for guards it already ran, makes the hook skip exactly those; a missing, malformed, or stale stamp skips none. - Installs migrate on their own. Re-running the installer strips the old per-guard registrations from
settings.json/hooks.jsonand registers the merged hook; uninstall removes the merged entry and any legacy one. Install-verify now separates the shipped files (the five guard modules are still copied) from the registered entrypoints.
Fixed
- Mockup-link guard accepts an approval question that carries the link (#209). Claude Code writes the assistant message to the transcript only after the tool call, so the guard never saw the prose with the mockup link and blocked every approval ask. It now accepts a link in the question, header or options, and judges the prose only when it can find the pending ask in the transcript.
- Secret guard follows a body file passed through a shell variable (#210).
--body-file "$D/x.md"was read verbatim and missed; writes behind another statement (D=... && gh ...,cd x && gh ...) were not seen as writes. Every statement of a compound command is now analysed and body sources (files, stdin, heredocs, inline bodies,gh apifields) are resolved with shell semantics. - Body guard sees an empty-body write chained after another command (#211). It now walks every statement like the secret guard; on a partial install it falls back to the single-statement check instead of allowing everything.
- Shell scan no longer reads
<<in arithmetic or quotes as a heredoc (#212). A fake heredoc used to swallow every later line, hiding e.g.cat .envfrom the guards. One context-aware lexer (quotes,$((..)),${..},$(..), comments, escapes, continuations) now drives the heredoc walk and the statement splitters. - Guards see a gh write with a flag between the subcommand and the verb (#225).
gh issue -R o/r comment 5 --body <T>,gh pr --repo o/r comment 5 ...,gh issue --repo=o/r comment 5 ...andgh issue -R o/r edit 5 --body ""slipped past the secret and empty-body guards because the shared parser read the flag as the verb. Flags after the subcommand are now skipped with the same arity model as the global ones, so the real verb is classified. - Secret-read guard sees a heredoc fed to a shell interpreter (#223).
bash <<'EOF',sh <<EOFandcat <<EOF | bashwithcat .envin the body slipped past because a heredoc body was only ever data, and only the substitutions in an unquoted one were scanned. A body whose pipeline runsbash,sh,zsh,dashorksh(also behindNAME=valueorenv, with any flags or path) is now scanned as commands, whatever the delimiter quoting.cat <<'EOF',tee f <<'EOF'andcat <<EOF; bash x.shstay data, and wrappers such assudoornohupare not followed. - Secret-read guard sees a secret path split by a line continuation (#222).
cat \<nl>.env,echo $(cat \<nl>.env)andcat .e\<nl>nvslipped past because the statement splitter rejoined the lines with the backslash and a newline still in place, so the path token never matched. A live backslash-newline is now removed like bash does, in the splitter shared by the guards. A single-quoted continuation and an escaped backslash before the newline stay literal. - Secret guard sees a token rebuilt by brace expansion (#220). A credential written with an unquoted brace group (
--body <A>{,x}<B>,"<A>"{,x}"<B>",<A>{x,}<B>,x{<A>,y}<B>, or agh api -f body=<A>{,x}<B>field) slipped past because the guard read{,,and}literally while bash expands the word into<A><B> <A>x<B>before gh posts it. The guard now expands brace groups on the raw word like bash (comma lists, nesting, several groups,{x..y}and{x..y..step}sequences) before quote removal, both when it scans the statement words and when it traces a--body,-b,gh apifield, echo/printf or here-string value. Quoted or escaped braces,${...},{}and{x}stay literal, and unquoted heredoc bodies are not brace-expanded, as in bash. Sequences take signed ends and steps ({+0..0},{1..+3..+1}) and pad like bash ({-01..2}is-01 000 001 002). An expansion over 256 words (for example nine{a,b}groups or{1..999999999}), nesting deeper than 64 levels, or a word too costly to scan cannot be enumerated and fails closed with the unresolved-body block; an unexpected fault in the expansion blocks too instead of skipping the scan. Brace expansion applies to the whole gh argument list, as in bash: each statement's words are expanded in place before it is classified or its flags are read, so a group in a flag name, a field key, a global flag, the subcommand or the verb (-{f,f} body=<T>,--{field,field},-f b{ody,ody}=<T>,{gh,gh} issue comment,gh -R o/r {issue,issue} comment) no longer hides a write; read-only groups such asgh issue {view,list} 5still pass. A group that only builds a write when one alternative is taken, or a gh statement too large to expand that has a write verb or anapibody word (aghword, not just the letters inside another word), is blocked as an opaque write; a read-only one such asgh issue view {1..300}and a non-gh command such asecho {a..z}{a..z}{a..z} highpass. The block message says "failed" instead of "too large" when the expansion itself faulted. Brace groups inside$(...),<(...),>(...)and backtick spans are left alone, as bash does. - Body guard resolves repeated body flags like gh (#213).
gh issue edit/gh pr editwith a repeated--body/--body-file(or-b/-F) was judged on the first occurrence, but gh keeps the last, so--body "x" --body ""slipped through while--body "" --body "x"was blocked. The guard now takes the last value of each flag, treats an empty--body-fileas absent, and allows--bodycombined with a non-empty--body-file(gh rejects it and writes nothing). - Secret-read guard sees a secret read inside
"$(..)"(#214).echo "$(cat .env)",x="$(cat .env)"andecho "$(true; cat .env)"slipped past because the double-quoted substitution hid the read from the per-token scan. The guard now also scans the body of every outermost$(..),<(..)and>(..)as a command of its own (nested ones recurse, within a depth and size cap that blocks on a secret-looking token); that includes a substitution in a heredoc-bearing body or in arithmetic. Single-quoted text stays literal. - Secret-read guard sees a secret read inside a backtick substitution (#216).
echo "cat .env",x="cat .env"andecho "${x:-cat .env}"slipped past because the lexer did not track backticks, so a quoted backtick body was never scanned. The guard now also scans the body of every outermost backtick span as a command of its own (an escaped backtick is not a substitution, and single-quoted text,$'..'and comments stay literal). The span is lexed as an opaque body (a quote inside it cannot desync the scanner, and a;or newline inside it no longer cuts a statement), and\"in a body inside double quotes is unescaped as the shell does. - Secret-read guard sees a secret read inside an unquoted heredoc body (#218).
cat <<EOFwith$(cat .env)or a backtickcat .envin its body slipped past because heredoc bodies were never scanned. They are now scanned as commands of their own, lexed as the shell reads a heredoc body (quotes are literal there, so an apostrophe before the substitution no longer hides it), while a quoted delimiter (<<'EOF',<<"EOF") and an escaped\$(..)stay literal. A line continuation inside the body is joined as bash does, and a heredoc nested in a substitution in the body keeps its own body and terminator. A backticked secret name in an unquoted heredoc (for example prose in a commit-message heredoc) is now treated as a command and blocked, so use a quoted delimiter (<<'EOF') for literal text. - Secret guard sees a gh write with a quoted command word or field key (#219). A write whose command word, subcommand, verb or
gh apifield key was quoted, split or escaped ("gh" issue comment,gh 'issue' comment,gh issue com""ment,\gh ...,$'gh' ...,-f 'body'=<T>,-F bo""dy=@file) was allowed while the unquoted form blocked, because the guard classified the statement from raw, still-quoted tokens and bash removes the quotes beforeghruns. The same gap hid a quoted wrapper or shell runner ('bash' -c ...,"env" "bash" -c ..., which now block as an opaque write like the bare form), thebody=@fileread behind a quoted key, and the unresolved-command-word fallback ($CMD 'issue' comment ...). The guard now matches on the quote-removed words ($before a quote, backslash escapes and quote characters dropped), erring toward detection; reserved words such astimeorcaseare not quote-resolved, as a quoted one is not a keyword in bash. The empty-body guard shares the detector, sogh "issue" edit 5 --body ""is caught there too. The same matching now covers a quoted or split flag name (gh api x '--input' F,env "-i" gh ...,sudo "-u" root gh ...,'-F' F,--body-fi""le F,"--body-file=F"), and ANSI-C escapes in a command word or verb are decoded ($'\x67h' issue comment,gh issue $'comm\x65nt'). A quoted attached short body-file flag is read as the flag too ("-F/path",-"F"/path,"-F/my dir/b.txt"), while a quoted value with a space right after the flag letter stays text (--title "-F handling"); a quoted value equal to a body-file flag (--body '--body-file=x',--title "-Fixes") blocks, like its unquoted form. - Secret guard sees a token split by adjacent quoted parts (#217). A credential broken across quotes (
--body "<A>""<B>","<A>"<B>,<A>''<B>, ANSI-C or locale quoting such as$'<A>'$'<B>'and$"<A>"$"<B>", a mix with a backslash-newline, or agh api -f body="<A>"'<B>'field) slipped past because the raw command keeps the quotes between the halves while bash joins them into one word. The guard now also scans every statement word with its quotes resolved; a word it cannot expand falls back to plain quote removal, and variables are not expanded there. Separate quoted words stay separate. A$'span opens only after a bare$, so$$'x\'(PID, then a quote) no longer hides a later write from the statement splitter, also across a backslash-newline continuation ($\<nl>$'x\'is a PID,$\<nl>'x\'an ANSI-C span). The${,$[and$(openers follow the same rule, so$${ ; <write> ; echo }and$$[ ; <write> ; echo ](PID plus a literal char) no longer hide the write between them. - Secret guard sees a token split by a backslash-newline (#215). A credential broken by a
\<newline>continuation (--body "ghp_A\<nl>B", an unquoted one, a--title, or an unquoted heredoc body) slipped past because the raw command still had it split while bash joins it before gh posts it. The guard now also scans each statement and each unquoted heredoc body with the continuations removed, andexpandPlaindrops the pair like bash. Single-quoted text, quoted heredocs and an escaped backslash before the newline stay literal.