@marcomigozzi/trailhead 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.it.md +6 -3
  2. package/README.md +6 -3
  3. package/bin/lib/codex-projection.js +2 -1
  4. package/bin/lib/codex-projection.test.js +4 -1
  5. package/bin/trailhead.js +23 -13
  6. package/bin/trailhead.test.js +328 -0
  7. package/package.json +1 -1
  8. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  9. package/plugins/trailhead/commands/adopt.md +5 -1
  10. package/plugins/trailhead/commands/audit.md +10 -0
  11. package/plugins/trailhead/commands/auto.md +5 -1
  12. package/plugins/trailhead/commands/bug.md +5 -1
  13. package/plugins/trailhead/commands/config.md +5 -1
  14. package/plugins/trailhead/commands/dashboard.md +5 -1
  15. package/plugins/trailhead/commands/grill.md +5 -1
  16. package/plugins/trailhead/commands/idea.md +5 -1
  17. package/plugins/trailhead/commands/inbox.md +5 -1
  18. package/plugins/trailhead/commands/map.md +5 -1
  19. package/plugins/trailhead/commands/new.md +5 -1
  20. package/plugins/trailhead/commands/note.md +5 -1
  21. package/plugins/trailhead/commands/pause.md +5 -1
  22. package/plugins/trailhead/commands/prune.md +5 -1
  23. package/plugins/trailhead/commands/quick.md +6 -2
  24. package/plugins/trailhead/commands/resume.md +5 -1
  25. package/plugins/trailhead/commands/seed.md +5 -1
  26. package/plugins/trailhead/commands/split.md +5 -1
  27. package/plugins/trailhead/commands/ticket.md +5 -1
  28. package/plugins/trailhead/commands/todo.md +5 -1
  29. package/plugins/trailhead/commands/update.md +5 -1
  30. package/plugins/trailhead/commands/whiteboard.md +5 -1
  31. package/plugins/trailhead/commands/work.md +5 -1
  32. package/plugins/trailhead/hooks/hooks.json +9 -0
  33. package/plugins/trailhead/hooks/lib/audit-walk.test.js +802 -0
  34. package/plugins/trailhead/hooks/lib/fixtures/label-guard-0.12.0.yml +55 -0
  35. package/plugins/trailhead/hooks/lib/label-guard-sync.test.js +156 -0
  36. package/plugins/trailhead/hooks/lib/mockup-link.js +50 -0
  37. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +56 -4
  38. package/plugins/trailhead/hooks/lib/model-defaults.json +5 -1
  39. package/plugins/trailhead/hooks/trailhead-check-update.js +25 -1
  40. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +95 -0
  41. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +145 -0
  42. package/plugins/trailhead/hooks/trailhead-mockup-link-stop.js +38 -0
  43. package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +64 -0
  44. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +151 -6
  45. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +56 -0
  46. package/plugins/trailhead/skills/_shared/choices.md +2 -0
  47. package/plugins/trailhead/skills/_shared/configuration-reference.md +3 -3
  48. package/plugins/trailhead/skills/_shared/load-first.md +2 -0
  49. package/plugins/trailhead/skills/_shared/principles.md +1 -1
  50. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  51. package/plugins/trailhead/skills/_shared/substrate-commands.md +9 -2
  52. package/plugins/trailhead/skills/_shared/teamwork.md +3 -1
  53. package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
  54. package/plugins/trailhead/skills/trailhead/SKILL.md +2 -2
  55. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
  56. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +5 -4
  57. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +132 -0
  58. package/plugins/trailhead/skills/trailhead-manage/references/updating.md +4 -0
  59. package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
  60. package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -1
  61. package/plugins/trailhead/templates/trailhead-audit-walk.js +685 -0
  62. package/plugins/trailhead/templates/trailhead-label-guard-sync.js +212 -0
  63. package/plugins/trailhead/templates/trailhead-label-guard.yml +5 -0
package/README.it.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  🇬🇧 [English](README.md) · 🇮🇹 **Italiano**
8
8
 
9
- [![sito](https://img.shields.io/badge/sito-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![CI sito](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=CI%20sito)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![host](https://img.shields.io/badge/host-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![comandi](https://img.shields.io/badge/comandi-22%20verbi-0e8a16?style=flat-square) ![guardrail](https://img.shields.io/badge/guardrail-7%20hook-e8710a?style=flat-square)
9
+ [![sito](https://img.shields.io/badge/sito-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![CI sito](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=CI%20sito)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![host](https://img.shields.io/badge/host-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![comandi](https://img.shields.io/badge/comandi-23%20verbi-0e8a16?style=flat-square) ![guardrail](https://img.shields.io/badge/guardrail-7%20hook-e8710a?style=flat-square)
10
10
 
11
11
  </div>
12
12
 
@@ -294,6 +294,8 @@ Tre layer: **il più vicino vince**, chiave per chiave (una chiave non impostata
294
294
 
295
295
  `/trailhead:prune [map]` recupera gli slot di sub-issue di una mappa sotto il cap di 100 di GitHub (elimina gli edge chiusi/orfani, mantiene ogni riferimento). Offerto automaticamente quando una mappa è vicina o al cap, ed eseguibile anche su richiesta.
296
296
 
297
+ `/trailhead:audit [mappa] [--all]` cerca le regressioni: verifica che i ticket chiusi dall'ultimo audit siano ancora veri rispetto al codice attuale. Il codice che i commit di un ticket non hanno più toccato è classificato invariato, senza agente (un controllo git veloce e deterministico che si basa sui trailer `Refs: #<n>` dei commit), decisioni, ricerche, task e prototipi ricevono un controllo leggero inline, e un agente di verifica completo parte solo dove il codice è cambiato o il controllo veloce non può decidere (per esempio senza commit `Refs:`). Ogni ticket è classificato come implementato, invariato, evoluto (un ticket successivo l'ha cambiato di proposito, elencato senza proposte), implementato male o non implementato. Poi confronta quelli aperti per trovare duplicati e cluster raggruppabili. Resta in sola lettura finché non confermi: ogni correzione (un bug di regressione, una riapertura, la chiusura di un duplicato, un supersede-and-merge) è una proposta applicata solo con un sì esplicito. `--all` riverifica tutti i ticket chiusi.
298
+
297
299
  Il passo **📊 statusline** offre di installare la status bar di trailhead per Claude Code: una riga con **model · project · branch · plan usage (`5h %` · reset · `7d %`) · una barra della context-window**, più una **seconda riga con il ticket attivo** (`▸ #N Title`) ogni volta che ne stai lavorando uno, e un flag `⬆ trailhead <version>` quando è disponibile un trailhead più recente (esegui `/trailhead:update`). Il project è sempre il nome del repo principale anche da un checkout isolato, e il branch porta un tag `(WT)` in un worktree o `(C)` in un clone per-ticket (niente sul checkout originale). È un'impostazione globale di Claude Code; se già usi una statusline (es. `ccstatusline`) il setup chiede prima di rimpiazzarla, e lo script espone anche i segmenti `--ticket-only` / `--context-only` / `--usage-only` da inserire in un tool esistente.
298
300
 
299
301
  **Offerta al primo uso (una volta per progetto).** Non devi andarla a cercare: la prima volta che configuri un progetto (alla fine di `/trailhead:new`/`:adopt`, o su un `/trailhead` nudo quando esiste una mappa ma non una config) trailhead offre di eseguire il setup guidato, o di continuare sui default. Chiede **solo una volta**: configurare scrive `.trailhead/config.json`, e "continua sui default" scrive un `{}` vuoto. Una volta che quel file esiste non viene più offerto, e non interrompe mai a metà ticket.
@@ -341,7 +343,7 @@ Se l'MCP della modalità hosted non è connesso, trailhead **offre di installarl
341
343
 
342
344
  ## 🪝 Hooks
343
345
 
344
- Oltre alle istruzioni della skill, trailhead include sette hook propri (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) che *impongono* le parti della disciplina che non ci si dovrebbe fidare che il modello ricordi:
346
+ Oltre alle istruzioni della skill, trailhead include otto hook propri (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) che *impongono* le parti della disciplina che non ci si dovrebbe fidare che il modello ricordi:
345
347
 
346
348
  - **Commit guard** (`PreToolUse` su `git commit`): blocca in modo netto un commit il cui subject non è [Conventional Commits](https://www.conventionalcommits.org), e blocca in modo netto qualsiasi trailer `Co-Authored-By`. Questo rispecchia la validazione dei commit di GSD.
347
349
  - **Secret guard** (`PreToolUse` sulle scritture `gh` di issue/PR): trailhead pubblica molto sul tracker (corpi dei ticket, commenti del motore, issue codebase/conventions, risoluzioni). Questo scansiona ciò che una scrittura `gh issue`/`gh pr`/`gh api` sta per pubblicare (l'inline `--body`, un heredoc, o un `--body-file`) e lo **blocca in modo netto** se corrisponde a un pattern di credenziale (chiavi private, token GitHub/AWS/OpenAI/Slack/Google/Stripe, JWT, o un `password`/`secret`/`token=…` hardcoded). Il blocco è un **segnale di pulisci-e-riprova, non un vicolo cieco**: trailhead redige il valore segnalato sul posto (`<REDACTED>` o un riferimento a env-var) e ripubblica automaticamente, così il contenuto pulito atterra comunque: il segreto semplicemente non raggiunge mai il tracker. Resta un *blocco* fail-safe anziché una silenziosa auto-riscrittura di proposito: un blocco non fa mai leak, mentre una redazione che silenziosamente non è stata applicata sì. Non scansiona mai le letture, solo le scritture in uscita.
@@ -349,9 +351,10 @@ Oltre alle istruzioni della skill, trailhead include sette hook propri (in `hook
349
351
  - **Search guard** (`PreToolUse` su `Bash`): un backstop strutturale per la regola di igiene dei comandi di ricerca (mai `cd` seguito da una lettura/ricerca su percorso relativo). Blocca in modo netto un singolo comando Bash che sia insieme un cambio di directory (`cd`/`pushd`/`env -C`) e, in un'istruzione unita o successiva, una lettura/ricerca su percorso relativo (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, o `git grep`/`log`/`show`/`diff` senza `-C`), così la forma che fa scattare una regola di deny su `Read()` sotto bypass permissions (ripiegando su un prompt di approvazione manuale invece di girare headless) non viene mai eseguita, anziché essere solo scoraggiata nella prosa dell'agente. Solo Claude Code; non è proiettata su Codex, che non ha una regola di deny su `Read()`.
350
352
  - **Body guard** (`PreToolUse` sulle scritture `gh` di body issue/PR): blocca in modo netto una scrittura `gh issue edit` / `gh pr edit` / `gh api` che imposterebbe il body di un issue a vuoto o solo spazi, così un read-modify-write la cui lettura è tornata con un body vuoto/degradato (es. un HTTP 5xx transitorio su `gh issue view --json body`) non può mai cancellare il body esistente.
351
353
  - **Secret-read guard** (`PreToolUse` su `Read|Bash`): blocca in modo netto la lettura di `.env`/`.env.*`/`.secrets`, sia tramite il tool Read sia tramite un comando Bash (un operando file, il valore di un'opzione, o un target di redirect `<`). È una decisione `deny` a livello di hook anziché una regola di permesso su `Read()`, quindi i segreti restano illeggibili senza attivare il prompt di approvazione `cd-compound-read` di Claude Code 2.1.259.
354
+ - **Mockup-link guard** (`PreToolUse` su `AskUserQuestion`): rete di sicurezza strutturale per la regola della tecnica Prototype secondo cui la richiesta di approvazione di un mockup porta con sé il link al mockup. Blocca un `AskUserQuestion` che chiede di approvare un mockup (o di scegliere tra varianti) quando il testo della risposta subito prima non contiene un URL o un path, così l'agente mette il link cliccabile nel messaggio prima di richiedere. Su Codex, che non ha il tool `AskUserQuestion` e pone le domande come elenco numerato in testo, il gemello `mockup-link-stop` gira su `Stop`: se la risposta finale chiede quell'approvazione senza link, Codex fa proseguire il turno e l'agente riscrive la domanda col link.
352
355
  - **Issue injection scanner** (`PostToolUse` sulle letture `gh`): trailhead legge testo di issue/PR/commenti scritto da chiunque abbia accesso al repo, cioè input non fidato. Quando quel testo contiene frasi di prompt-injection, l'hook inietta un avviso che ricorda all'agente di trattarlo come dati, mai come comandi. Solo advisory: non blocca mai.
353
356
 
354
- Tutti e sette sono crash-safe (qualsiasi errore → allow) e attivi ogni volta che il plugin è installato. I guard di commit, secret, install, search, body e secret-read quindi si applicano in **ogni** repo, non solo nei progetti trailhead: intenzionale, dato che i conventional commit, niente `Co-Authored-By`, nessun segreto leakato, nessuna installazione non verificata, niente `cd`-poi-lettura-relativa, nessun body issue cancellato, e niente `.env`/`.secrets` leggibili sono regole permanenti di trailhead. Per fare opt-out, disabilita gli hook del plugin nelle impostazioni di Claude Code.
357
+ Tutti e otto sono crash-safe (qualsiasi errore → allow) e attivi ogni volta che il plugin è installato. I guard di commit, secret, install, search, body e secret-read quindi si applicano in **ogni** repo, non solo nei progetti trailhead: intenzionale, dato che i conventional commit, niente `Co-Authored-By`, nessun segreto leakato, nessuna installazione non verificata, niente `cd`-poi-lettura-relativa, nessun body issue cancellato, e niente `.env`/`.secrets` leggibili sono regole permanenti di trailhead. Per fare opt-out, disabilita gli hook del plugin nelle impostazioni di Claude Code.
355
358
 
356
359
  ---
357
360
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  🇬🇧 **English** · 🇮🇹 [Italiano](README.it.md)
8
8
 
9
- [![website](https://img.shields.io/badge/website-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![site CI](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=site%20CI)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![hosts](https://img.shields.io/badge/hosts-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![commands](https://img.shields.io/badge/commands-22%20verbs-0e8a16?style=flat-square) ![guardrails](https://img.shields.io/badge/guardrails-7%20hooks-e8710a?style=flat-square)
9
+ [![website](https://img.shields.io/badge/website-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![site CI](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=site%20CI)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![hosts](https://img.shields.io/badge/hosts-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![commands](https://img.shields.io/badge/commands-23%20verbs-0e8a16?style=flat-square) ![guardrails](https://img.shields.io/badge/guardrails-7%20hooks-e8710a?style=flat-square)
10
10
 
11
11
  </div>
12
12
 
@@ -294,6 +294,8 @@ Three layers: **nearest wins**, key by key (a key unset at one layer inherits th
294
294
 
295
295
  `/trailhead:prune [map]` reclaims a map's sub-issue slots under GitHub's 100-cap (drops closed/orphan edges, keeps every reference). Offered automatically when a map is near or at the cap, and runnable on demand.
296
296
 
297
+ `/trailhead:audit [map] [--all]` hunts regressions: it checks that the tickets closed since the last audit are still true against the current code. Code a ticket's commits left untouched is classed unchanged with no agent (a fast, deterministic git check that relies on the `Refs: #<n>` commit trailers), decisions, research, tasks and prototypes get a light inline check, and a full verification agent runs only where the code changed or the fast path cannot decide (no `Refs:` commits, for example). Each ticket is classed implemented, unchanged, evolved (a later ticket changed it on purpose, listed with no proposal), implemented wrong, or not implemented. It then compares the open ones for duplicates and groupable clusters. It is read-only until you confirm: every fix (a regression bug, a reopen, a duplicate close, a supersede-and-merge) is a proposal applied only on an explicit yes. `--all` re-audits every closed ticket.
298
+
297
299
  The **📊 statusline** step offers to install trailhead's Claude Code status bar: one line with **model · project · branch · plan usage (`5h %` · reset · `7d %`) · a context-window bar**, plus a **second line with the active ticket** (`▸ #N Title`) whenever you're working one, and a `⬆ trailhead <version>` flag when a newer trailhead is available (run `/trailhead:update`). The project is always the main repo's name even from an isolated checkout, and the branch carries a `(WT)` tag in a worktree or `(C)` in a per-ticket clone (nothing on the original checkout). It's a global Claude Code setting; if you already run a statusline (e.g. `ccstatusline`) the setup asks before replacing it, and the script also exposes `--ticket-only` / `--context-only` / `--usage-only` segments to slot into an existing tool.
298
300
 
299
301
  **First-use offer (once per project).** You don't have to seek this out: the first time you set a project up (at the end of `/trailhead:new`/`:adopt`, or on a bare `/trailhead` when a map exists but no config does) trailhead offers to run the guided setup, or to continue on defaults. It asks **only once**: configuring writes `.trailhead/config.json`, and "continue on defaults" writes an empty `{}`. Once that file exists it's never offered again, and it never interrupts mid-ticket.
@@ -341,7 +343,7 @@ If a hosted mode's MCP isn't connected, trailhead **offers to install it once**
341
343
 
342
344
  ## 🪝 Hooks
343
345
 
344
- Beyond the skill's instructions, trailhead ships seven of its own hooks (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) that *enforce* the parts of the discipline the model shouldn't be trusted to remember:
346
+ Beyond the skill's instructions, trailhead ships eight of its own hooks (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) that *enforce* the parts of the discipline the model shouldn't be trusted to remember:
345
347
 
346
348
  - **Commit guard** (`PreToolUse` on `git commit`): hard-blocks a commit whose subject isn't [Conventional Commits](https://www.conventionalcommits.org), and hard-blocks any `Co-Authored-By` trailer. This mirrors GSD's commit validation.
347
349
  - **Secret guard** (`PreToolUse` on `gh` issue/PR writes): trailhead posts a lot to the tracker (ticket bodies, engine comments, codebase/conventions issues, resolutions). This scans what a `gh issue`/`gh pr`/`gh api` write is about to post (the inline `--body`, a heredoc, or a `--body-file`) and **hard-blocks** it if it matches a credential pattern (private keys, GitHub/AWS/OpenAI/Slack/Google/Stripe tokens, JWTs, or a hardcoded `password`/`secret`/`token=…`). The block is a **clean-and-retry cue, not a dead end**: trailhead redacts the flagged value in place (`<REDACTED>` or an env-var reference) and re-posts automatically, so the cleaned content still lands: the secret just never reaches the tracker. It stays a fail-safe *block* rather than a silent auto-rewrite on purpose: a block never leaks, whereas a redaction that quietly failed to apply would. It never scans reads, only outbound writes.
@@ -349,9 +351,10 @@ Beyond the skill's instructions, trailhead ships seven of its own hooks (in `hoo
349
351
  - **Search guard** (`PreToolUse` on `Bash`): structural backstop for the search-command-hygiene rule (never `cd` then read/search a relative path). It hard-blocks a single Bash command that both changes directory (`cd`/`pushd`/`env -C`) and, in a joined or later statement, reads/searches a relative path (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, or `git grep`/`log`/`show`/`diff` without a `-C`), so the shape that trips a `Read()` deny rule under bypass permissions (falling back to a manual approval prompt instead of running headless) never runs at all, rather than merely being discouraged in the agent's own prose. Claude Code only; not projected to Codex, which has no such `Read()` deny rule.
350
352
  - **Body guard** (`PreToolUse` on `gh` issue/PR body writes): hard-blocks a `gh issue edit` / `gh pr edit` / `gh api` write that would set an issue body to empty or whitespace-only, so a read-modify-write whose read returned an empty/degraded body (e.g. a transient HTTP 5xx on `gh issue view --json body`) can never clobber the existing body.
351
353
  - **Secret-read guard** (`PreToolUse` on `Read|Bash`): denies reads of `.env`/`.env.*`/`.secrets`, whether via the Read tool or a Bash command (a file operand, an option value, or a `<` redirect target). This is a hook `deny` decision rather than a `Read()` permission rule, so secrets stay unreadable without arming Claude Code 2.1.259's `cd-compound-read` approval prompt.
354
+ - **Mockup-link guard** (`PreToolUse` on `AskUserQuestion`): structural backstop for the Prototype rule that a mockup approval ask carries the mockup's link. It hard-blocks an `AskUserQuestion` that asks to approve a mockup (or pick between mockup variants) when the reply text right before it holds no URL or file path, so the agent puts the clickable link in the message before asking again. On Codex, which has no `AskUserQuestion` tool and asks as a plain-text numbered list, its twin `mockup-link-stop` runs on `Stop`: when the final reply asks for that approval without a link, Codex continues the turn so the agent rewrites the ask with it.
352
355
  - **Issue injection scanner** (`PostToolUse` on `gh` reads): trailhead reads issue/PR/comment text written by anyone with repo access, i.e. untrusted input. When that text contains prompt-injection phrases, the hook injects an advisory reminding the agent to treat it as data, never as commands. Advisory only: it never blocks.
353
356
 
354
- All seven are crash-safe (any error → allow) and active whenever the plugin is installed. The commit, secret, install, search, body, and secret-read guards therefore apply in **every** repo, not only trailhead projects: intentional, since conventional commits, no `Co-Authored-By`, no leaked secrets, no unvetted installs, no `cd`-then-relative-read, no clobbered issue bodies, and no readable `.env`/`.secrets` are trailhead's standing rules. To opt out, disable the plugin's hooks in your Claude Code settings.
357
+ All eight are crash-safe (any error → allow) and active whenever the plugin is installed. The commit, secret, install, search, body, and secret-read guards therefore apply in **every** repo, not only trailhead projects: intentional, since conventional commits, no `Co-Authored-By`, no leaked secrets, no unvetted installs, no `cd`-then-relative-read, no clobbered issue bodies, and no readable `.env`/`.secrets` are trailhead's standing rules. To opt out, disable the plugin's hooks in your Claude Code settings.
355
358
 
356
359
  ---
357
360
 
@@ -113,7 +113,7 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
113
113
  The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
114
114
 
115
115
  ## F. Lifecycle hooks (native Codex hooks)
116
- Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard and body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
116
+ Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard and body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
117
117
 
118
118
  ## G. Design / UI mockups -> local disk
119
119
  The \`claude.ai/design\` mockup mode (DesignSync: the \`claude_design\` MCP + the \`/design-sync\` skill) does not exist on Codex, so it **collapses to local-disk mockups** here: whatever \`config.design\` says, the Prototype technique's \`disk\` path is the only one available. Produce a throwaway static HTML mockup next to the code, link it from the ticket, and never attempt DesignSync / claude.ai/design on Codex (there is no \`claude_design\` MCP or \`/design-sync\` skill to call). \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
@@ -378,6 +378,7 @@ function codexHookEntries(hooksScriptsDir) {
378
378
  { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-body-guard.js') },
379
379
  { event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
380
380
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
381
+ { event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
381
382
  { event: 'SessionStart', matcher: '', command: cmd('trailhead-check-update.js') },
382
383
  ];
383
384
  }
@@ -102,6 +102,7 @@ ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling
102
102
  ok('codexSkillAdapterHeader §D makes background+poll the hard cross-host default for reviews',
103
103
  header.includes('always run this way') && header.includes('hard cross-host default'));
104
104
  ok('codexSkillAdapterHeader §F mentions real Codex hooks', header.includes('real Codex hooks'));
105
+ ok('codexSkillAdapterHeader §F lists mockup-link-stop as a Stop hook', header.includes('mockup-link-stop as a `Stop` hook'));
105
106
  ok('codexSkillAdapterHeader §F no longer says Codex has no hook bus', !header.includes('Codex has no hook bus'));
106
107
  ok('codexSkillAdapterHeader §F does not list search-guard among the projected PreToolUse guards',
107
108
  !header.includes('search-guard and body-guard') && !header.includes('search-guard, body-guard'));
@@ -161,7 +162,7 @@ ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', nul
161
162
 
162
163
  // --- codexHookEntries ---
163
164
  const entries = codexHookEntries('/h');
164
- ok('codexHookEntries returns 7 entries', Array.isArray(entries) && entries.length === 7);
165
+ ok('codexHookEntries returns 8 entries', Array.isArray(entries) && entries.length === 8);
165
166
  ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
166
167
  e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
167
168
  ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
@@ -176,6 +177,8 @@ ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((
176
177
  e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
177
178
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
178
179
  e.event === 'PostToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-issue-injection-scanner.js')));
180
+ ok('codexHookEntries: mockup-link-stop is Stop', entries.some((e) =>
181
+ e.event === 'Stop' && e.command.includes('trailhead-mockup-link-stop.js')));
179
182
  ok('codexHookEntries: check-update is SessionStart', entries.some((e) =>
180
183
  e.event === 'SessionStart' && e.command.includes('trailhead-check-update.js')));
181
184
 
package/bin/trailhead.js CHANGED
@@ -243,21 +243,27 @@ function claudePaths(configDir) {
243
243
  settings: path.join(configDir, 'settings.json'),
244
244
  };
245
245
  }
246
- const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-secret-read-guard.js', 'trailhead-body-guard.js', 'trailhead-check-update.js'];
246
+ const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-secret-read-guard.js', 'trailhead-body-guard.js', 'trailhead-mockup-link-guard.js', 'trailhead-mockup-link-stop.js', 'trailhead-check-update.js'];
247
247
  // Runtime libs the hook scripts require (trailhead-commit-guard.js does
248
248
  // require('./lib/commit-message-check.js'); trailhead-search-guard.js and
249
249
  // trailhead-secret-read-guard.js both do require('./lib/shell-scan.js'), #135
250
250
  // follow-up; trailhead-body-guard.js and trailhead-secret-guard.js both do
251
251
  // require('./lib/gh-subcommand.js'), #153 follow-up; trailhead-check-update.js
252
252
  // does require('./lib/model-defaults-review.js'), which in turn reads the
253
- // sibling model-defaults.json, #186). Copied by name, never a recursive
253
+ // sibling model-defaults.json, #186; both mockup-link hooks do
254
+ // require('./lib/mockup-link.js')). Copied by name, never a recursive
254
255
  // sweep: the Claude hooks/lib dir is shared with other plugins, so we must
255
256
  // not clobber a co-tenant's lib nor ship our own *.test.js files.
256
- const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js', 'model-defaults-review.js', 'model-defaults.json'];
257
- // search-guard is Claude-Code-specific (#169): excluded from both the Codex
258
- // hook copy and the Codex verify spec's expected hookFiles, so a healthy
259
- // Codex install is never flagged for a script it never ships.
260
- const CODEX_HOOK_EXCLUDE = ['trailhead-search-guard.js'];
257
+ const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js', 'model-defaults-review.js', 'model-defaults.json', 'mockup-link.js'];
258
+ // search-guard is Claude-Code-specific (#169), and so is mockup-link-guard
259
+ // (it gates the AskUserQuestion tool, which Codex lacks): excluded from both
260
+ // the Codex hook copy and the Codex verify spec's expected hookFiles, so a
261
+ // healthy Codex install is never flagged for a script it never ships.
262
+ const CODEX_HOOK_EXCLUDE = ['trailhead-search-guard.js', 'trailhead-mockup-link-guard.js'];
263
+ // mockup-link-stop is its Codex twin (a Stop hook reading the final reply):
264
+ // Claude Code gates the AskUserQuestion call instead, so it is neither copied
265
+ // nor expected on a Claude install.
266
+ const CLAUDE_HOOK_EXCLUDE = ['trailhead-mockup-link-stop.js'];
261
267
 
262
268
  // Copy the hook scripts and their runtime libs into a destination hooks dir
263
269
  // (Claude's ~/.claude/hooks or Codex's skills/trailhead/hooks). Shared by both
@@ -371,6 +377,7 @@ function uninstallClaude(configDir) {
371
377
  stripHook(s, 'PreToolUse', 'trailhead-search-guard.js');
372
378
  stripHook(s, 'PreToolUse', 'trailhead-secret-read-guard.js');
373
379
  stripHook(s, 'PreToolUse', 'trailhead-body-guard.js');
380
+ stripHook(s, 'PreToolUse', 'trailhead-mockup-link-guard.js');
374
381
  stripHook(s, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
375
382
  stripHook(s, 'SessionStart', 'trailhead-check-update.js');
376
383
  writeJSON(P.settings, s);
@@ -395,7 +402,7 @@ function installClaude(configDir, { useSymlink }) {
395
402
  }
396
403
  place(path.join(SRC, 'commands'), P.commands, useSymlink);
397
404
  place(path.join(SRC, 'templates'), P.templates, useSymlink);
398
- copyHookScripts(P.hooksDir, { useSymlink });
405
+ copyHookScripts(P.hooksDir, { useSymlink, exclude: CLAUDE_HOOK_EXCLUDE });
399
406
  // Register the engine agents as Claude subagents: sweep any stale
400
407
  // trailhead-*.md first (a removed/renamed agent), then place the current set
401
408
  // by name. This is what makes config.models.<key> take effect on the
@@ -419,6 +426,7 @@ function installClaude(configDir, { useSymlink }) {
419
426
  addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-search-guard.js')),
420
427
  addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-body-guard.js')),
421
428
  addHook(s, 'PreToolUse', 'Read|Bash', hookCmd('trailhead-secret-read-guard.js')),
429
+ addHook(s, 'PreToolUse', 'AskUserQuestion', hookCmd('trailhead-mockup-link-guard.js')),
422
430
  addHook(s, 'PostToolUse', 'Bash', hookCmd('trailhead-issue-injection-scanner.js')),
423
431
  addHook(s, 'SessionStart', '', hookCmd('trailhead-check-update.js')),
424
432
  ].some(Boolean);
@@ -430,7 +438,7 @@ function installClaude(configDir, { useSymlink }) {
430
438
  const spec = claudeVerifySpec(configDir, {
431
439
  skillDirs: shippedSkillDirs(),
432
440
  sharedFiles: readSharedCoreFiles(),
433
- hookFiles: HOOK_FILES,
441
+ hookFiles: HOOK_FILES.filter((f) => !CLAUDE_HOOK_EXCLUDE.includes(f)),
434
442
  hookLibFiles: HOOK_LIB_FILES,
435
443
  env: process.env,
436
444
  home: os.homedir(),
@@ -501,14 +509,15 @@ function readCommandVerbs() {
501
509
  .filter((f) => f.endsWith('.md'))
502
510
  .map((f) => {
503
511
  const raw = fs.readFileSync(path.join(dir, f), 'utf8');
504
- return { verb: f.slice(0, -3), description: fmValue(raw, 'description'), argumentHint: fmValue(raw, 'argument-hint') };
512
+ return { verb: f.slice(0, -3), description: unquoteYamlScalar(fmValue(raw, 'description')), argumentHint: unquoteYamlScalar(fmValue(raw, 'argument-hint')) };
505
513
  })
506
514
  .sort((a, b) => a.verb.localeCompare(b.verb));
507
515
  }
508
516
 
509
- // Unquote a YAML double-quoted scalar (unlike commands/*.md, the agent .md
510
- // frontmatter wraps `description:` in double quotes). Leaves a plain/
511
- // unquoted scalar untouched, so this is safe to apply unconditionally.
517
+ // Unquote a YAML double-quoted scalar. Applied to both the agent .md and the
518
+ // commands/*.md frontmatter, since either may wrap values in double quotes.
519
+ // Leaves a plain/unquoted scalar untouched, so this is safe to apply
520
+ // unconditionally.
512
521
  function unquoteYamlScalar(v) {
513
522
  if (typeof v !== 'string' || v.length < 2 || v[0] !== '"' || v[v.length - 1] !== '"') return v;
514
523
  return v.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
@@ -774,6 +783,7 @@ function uninstallCodex(configDir) {
774
783
  stripHook(h, 'PreToolUse', 'trailhead-secret-read-guard.js');
775
784
  stripHook(h, 'PreToolUse', 'trailhead-body-guard.js');
776
785
  stripHook(h, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
786
+ stripHook(h, 'Stop', 'trailhead-mockup-link-stop.js');
777
787
  stripHook(h, 'SessionStart', 'trailhead-check-update.js');
778
788
  writeJSON(L.hooksJson, h);
779
789
  }