frizz 0.0.1 → 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/claude-agent-broker.js +21714 -0
- package/dist/codex-app-server-daemon.js +354 -0
- package/dist/dev-child.js +39342 -0
- package/dist/frizz.js +10517 -0
- package/package.json +54 -20
- package/runtime/board/agent-bindings.mjs +287 -0
- package/runtime/board/agent-liveness.mjs +367 -0
- package/runtime/board/agent-status.mjs +178 -0
- package/runtime/board/config.mjs +982 -0
- package/runtime/board/decisions.mjs +97 -0
- package/runtime/board/index.mjs +699 -0
- package/runtime/board/notify-shared.mjs +90 -0
- package/runtime/board/notify.mjs +81 -0
- package/runtime/board/ownership.mjs +120 -0
- package/runtime/board/rest-detect.mjs +213 -0
- package/runtime/board/thread-excerpt.mjs +162 -0
- package/runtime/board/thread-update.mjs +285 -0
- package/runtime/cc-worker/.claude-plugin/plugin.json +10 -0
- package/runtime/cc-worker/DECISIONS.md +1025 -0
- package/runtime/cc-worker/LICENSE +21 -0
- package/runtime/cc-worker/agents/fable-high.md +8 -0
- package/runtime/cc-worker/agents/fable-low.md +8 -0
- package/runtime/cc-worker/agents/fable-max.md +8 -0
- package/runtime/cc-worker/agents/fable-medium.md +8 -0
- package/runtime/cc-worker/agents/fable-xhigh.md +8 -0
- package/runtime/cc-worker/agents/haiku.md +7 -0
- package/runtime/cc-worker/agents/opus-high.md +8 -0
- package/runtime/cc-worker/agents/opus-low.md +8 -0
- package/runtime/cc-worker/agents/opus-max.md +8 -0
- package/runtime/cc-worker/agents/opus-medium.md +8 -0
- package/runtime/cc-worker/agents/opus-xhigh.md +8 -0
- package/runtime/cc-worker/agents/sonnet-high.md +8 -0
- package/runtime/cc-worker/agents/sonnet-low.md +8 -0
- package/runtime/cc-worker/agents/sonnet-max.md +8 -0
- package/runtime/cc-worker/agents/sonnet-medium.md +8 -0
- package/runtime/cc-worker/agents/sonnet-xhigh.md +8 -0
- package/runtime/cc-worker/bin/frizz +17 -0
- package/runtime/cc-worker/bin/frizz-mcp.mjs +564 -0
- package/runtime/cc-worker/bin/frizz-update +18 -0
- package/runtime/cc-worker/hooks/agent-bind.mjs +40 -0
- package/runtime/cc-worker/hooks/agent-dispatch.mjs +98 -0
- package/runtime/cc-worker/hooks/bash-background.d.mts +6 -0
- package/runtime/cc-worker/hooks/bash-background.mjs +236 -0
- package/runtime/cc-worker/hooks/deny-ask.mjs +38 -0
- package/runtime/cc-worker/hooks/deny-plan.mjs +61 -0
- package/runtime/cc-worker/hooks/hooks.json +111 -0
- package/runtime/cc-worker/hooks/perm-policy.mjs +211 -0
- package/runtime/cc-worker/hooks/precompact-instructions.mjs +122 -0
- package/runtime/cc-worker/hooks/scratchpad-stop.mjs +125 -0
- package/runtime/cc-worker/hooks/scratchpad.mjs +446 -0
- package/runtime/cc-worker/hooks/session-seed.mjs +106 -0
- package/runtime/cc-worker/scripts/frizz/agent-bindings.mjs +9 -0
- package/runtime/cc-worker/scripts/frizz/config.mjs +12 -0
- package/runtime/cc-worker/skills/gh/SKILL.md +154 -0
- package/runtime/cc-worker/skills/gh/scripts/ci-watch.mjs +60 -0
- package/runtime/cc-worker/skills/gh/scripts/github-watch.mjs +130 -0
- package/runtime/cc-worker/skills/gh/scripts/review-watch.mjs +54 -0
- package/runtime/cc-worker/skills/handoff/SKILL.md +209 -0
- package/runtime/cc-worker/skills/waits/SKILL.md +83 -0
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/TerminalPane-ROKHp1ib.js +7 -0
- package/web-dist/assets/abnfDiagram-VRR7QNED-DcpdhBs3.js +1 -0
- package/web-dist/assets/arc-BSyeo0Gb.js +1 -0
- package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +1 -0
- package/web-dist/assets/architectureDiagram-ZJ3FMSHR-CUAKf0mn.js +36 -0
- package/web-dist/assets/array-BifhSqXX.js +1 -0
- package/web-dist/assets/blockDiagram-677ZJIJ3-BPwpJIzx.js +132 -0
- package/web-dist/assets/c4Diagram-LMCZKHZV-1lptuHzZ.js +10 -0
- package/web-dist/assets/channel-CqKDIFQF.js +1 -0
- package/web-dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web-dist/assets/chunk-32BRIVSS-CFR9AKjY.js +1 -0
- package/web-dist/assets/chunk-52WLFC77-CM9uct7m.js +10 -0
- package/web-dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web-dist/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- package/web-dist/assets/chunk-C7G6YPKG-DiveJARw.js +1 -0
- package/web-dist/assets/chunk-EX3LRPZG-BE1CBw8F.js +231 -0
- package/web-dist/assets/chunk-FWX5IMBZ-DL42uXiO.js +2 -0
- package/web-dist/assets/chunk-HOUHSVGY-DPhJWgDw.js +1 -0
- package/web-dist/assets/chunk-ICXQ74PX-CwYy-6AP.js +2 -0
- package/web-dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web-dist/assets/chunk-KEIR6QF5-BfrZ3jm6.js +161 -0
- package/web-dist/assets/chunk-MOJQB5TN-Ds5I9wxq.js +88 -0
- package/web-dist/assets/chunk-OGEWGWER-DHiZJwQD.js +1 -0
- package/web-dist/assets/chunk-PUDLZKDR-B-eyQTsF.js +156 -0
- package/web-dist/assets/chunk-Q4XR5HBZ-DK7dB3Ti.js +70 -0
- package/web-dist/assets/chunk-RYQCIY6F-Cu_KplZW.js +1 -0
- package/web-dist/assets/chunk-V7JOEXUC-Dn59m74L.js +206 -0
- package/web-dist/assets/chunk-VAUOI2AC-DK7x36hd.js +1 -0
- package/web-dist/assets/chunk-VR4S4FIN-D7-CI3Yl.js +1 -0
- package/web-dist/assets/chunk-WYO6CB5R-B3l-mLCs.js +127 -0
- package/web-dist/assets/chunk-XXDRQBXY-DYlTP5J-.js +1 -0
- package/web-dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web-dist/assets/chunk-ZGVPDNZ5-Dobxlxie.js +62 -0
- package/web-dist/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +1 -0
- package/web-dist/assets/cose-bilkent-JH36ORCC-BUIsLrGc.js +1 -0
- package/web-dist/assets/cynefin-VYW2F7L2-Dh7RuEUJ.js +1 -0
- package/web-dist/assets/cynefinDiagram-TSTJHNR4-ClPi2mZZ.js +62 -0
- package/web-dist/assets/cytoscape.esm-B3I8pqwA.js +321 -0
- package/web-dist/assets/dagre-CXRCoUWR.js +1 -0
- package/web-dist/assets/dagre-VKFMJZFB-52_WP1QV.js +4 -0
- package/web-dist/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/web-dist/assets/diagram-FQU43EPY-D_1zVsTL.js +3 -0
- package/web-dist/assets/diagram-G47NLZAW-CdZxuGUy.js +24 -0
- package/web-dist/assets/diagram-NH7WQ7WH-C8pSFu0P.js +24 -0
- package/web-dist/assets/diagram-OA4YK3LP-C5bjZLre.js +30 -0
- package/web-dist/assets/diagram-WEI45ONY-Bxzhiuzn.js +41 -0
- package/web-dist/assets/dist-DoH_9pyS.js +1 -0
- package/web-dist/assets/ebnfDiagram-CCIWWBDH-g-Z0J2wP.js +1 -0
- package/web-dist/assets/erDiagram-Q63AITRT-DVNkgIHp.js +85 -0
- package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +1 -0
- package/web-dist/assets/ganttDiagram-NO4QXBWP-_71pQYEK.js +292 -0
- package/web-dist/assets/gitGraph-TEB2WS4Q-ChIZiGZS.js +1 -0
- package/web-dist/assets/gitGraphDiagram-IHSO6WYX-DCHAFI0l.js +106 -0
- package/web-dist/assets/graphlib-B8gBHxth.js +1 -0
- package/web-dist/assets/index-w4v-GZEc.js +358 -0
- package/web-dist/assets/index-zyi22LPz.css +1 -0
- package/web-dist/assets/info-DKCQHKI2-BW-n_T1j.js +1 -0
- package/web-dist/assets/infoDiagram-FWYZ7A6U-CgDYsKi9.js +2 -0
- package/web-dist/assets/init-D6jRqBbL.js +1 -0
- package/web-dist/assets/ishikawaDiagram-FXEZZL3T-ClzGNt9N.js +70 -0
- package/web-dist/assets/journeyDiagram-5HDEW3XC-DSCQxkHC.js +139 -0
- package/web-dist/assets/kanban-definition-HUTT4EX6-CdrdX9N8.js +89 -0
- package/web-dist/assets/katex-CddkPoXu.js +257 -0
- package/web-dist/assets/line-ha38Dc-1.js +1 -0
- package/web-dist/assets/linear-z2V0wJk9.js +1 -0
- package/web-dist/assets/map-DsCK-0Cs.js +1 -0
- package/web-dist/assets/mermaid-parser.core-Z4uMcpip.js +7 -0
- package/web-dist/assets/mermaid.core-iZRq3hbu.js +11 -0
- package/web-dist/assets/mindmap-definition-LN4V7U3C-DmhInJO_.js +96 -0
- package/web-dist/assets/ordinal-hYBb2elL.js +1 -0
- package/web-dist/assets/packet-7NZHBO7P-DBPB36Kl.js +1 -0
- package/web-dist/assets/path-BWPyau1x.js +1 -0
- package/web-dist/assets/pegDiagram-2B236MQR-CAH3ljfj.js +1 -0
- package/web-dist/assets/pie-RZYD4A2V-_h_eX4Ca.js +1 -0
- package/web-dist/assets/pieDiagram-ENE6RG2P-DFBPus8j.js +39 -0
- package/web-dist/assets/quadrantDiagram-ABIIQ3AL-DMvOCjt8.js +7 -0
- package/web-dist/assets/radar-I7S5WNFK-2EzoPHEZ.js +1 -0
- package/web-dist/assets/railroad-3IZDKUUU-BPJnn-hm.js +1 -0
- package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +1 -0
- package/web-dist/assets/railroadDiagram-RFXS5EU6-DKq5FagA.js +1 -0
- package/web-dist/assets/requirementDiagram-TGXJPOKE-BJ5tGazp.js +84 -0
- package/web-dist/assets/rolldown-runtime-Bh1tDfsg.js +1 -0
- package/web-dist/assets/rough.esm-CSKSodPl.js +1 -0
- package/web-dist/assets/sankeyDiagram-HTMAVEWB-XSJjcBhX.js +40 -0
- package/web-dist/assets/sequenceDiagram-DBY2YBRQ-CBb8emSe.js +162 -0
- package/web-dist/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- package/web-dist/assets/src-C4XfhTaE.js +1 -0
- package/web-dist/assets/stateDiagram-2N3HPSRC-DDfRW94V.js +1 -0
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +1 -0
- package/web-dist/assets/swimlanes-5IMT3BWC-DvRYbkZi.js +2 -0
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-BZyGdgSG.js +8 -0
- package/web-dist/assets/timeline-definition-FHXFAJF6-BNUa_DwI.js +120 -0
- package/web-dist/assets/treeView-QDETBFTQ-I6-IW6nJ.js +1 -0
- package/web-dist/assets/treemap-6X3UGDF4-CWWmEUYJ.js +1 -0
- package/web-dist/assets/vennDiagram-L72KCM5P-DTDrPGLk.js +34 -0
- package/web-dist/assets/wardley-OPB4EBWU-CNsdgXXA.js +1 -0
- package/web-dist/assets/wardleyDiagram-EHGQE667-YE0tq3Kh.js +78 -0
- package/web-dist/assets/xychartDiagram-FW5EYKEG-D0ofMX8C.js +7 -0
- package/web-dist/favicon-16.png +0 -0
- package/web-dist/favicon-32.png +0 -0
- package/web-dist/favicon.svg +78 -0
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/icon-maskable-512.png +0 -0
- package/web-dist/index.html +33 -0
- package/web-dist/manifest.webmanifest +16 -0
- package/index.d.ts +0 -1
- package/index.js +0 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// PreToolUse hook on the `Agent` tool (frizz-worker). A worker MAY spin up its own helper
|
|
4
|
+
// sub-agents; this hook holds them to the same rules cc enforces for the orchestrator:
|
|
5
|
+
// 1) ENFORCE background dispatch — deny any Agent call lacking run_in_background:true (a
|
|
6
|
+
// foreground agent blocks the worker's turn; a human interjection orphans it).
|
|
7
|
+
// 2) STRIP `name`/`team_name` — setting either strands a nested dispatch (its result routes
|
|
8
|
+
// wrong and never returns cleanly), so scrub both silently.
|
|
9
|
+
// 3) AUTO-APPEND a repo-neutral ORCHESTRATION EPILOGUE so helpers return a useful handoff,
|
|
10
|
+
// know how to reach their dispatcher mid-flight, do NOT fan out a layer of their own unless
|
|
11
|
+
// their prompt asked for it, and collect a helper correctly when it did — without imposing
|
|
12
|
+
// build, test, git, compilation, or process-lifecycle policy on arbitrary repos.
|
|
13
|
+
//
|
|
14
|
+
// WHY NESTING IS DEFAULT-OFF (2026-08-04, maintainer's call): the epilogue used to speak about a
|
|
15
|
+
// helper's own helper only in the conditional ("if you dispatch a helper of your own…"), which reads
|
|
16
|
+
// as neutral permission, so a depth-1 child could decompose again purely because it could. A child is
|
|
17
|
+
// already one prong of a fan-out; another layer splits the context its dispatcher assembled and moves
|
|
18
|
+
// the real work further from the board. So the paragraph now LEADS with "do the work yourself unless
|
|
19
|
+
// your prompt says otherwise" and keeps the collection rules for the case where it does. This is a
|
|
20
|
+
// PROMPT-level default, not the hook-level depth-2 DENY rejected on 2026-07-31 as too blunt — an
|
|
21
|
+
// explicit instruction to fan out still dispatches, unmodified.
|
|
22
|
+
//
|
|
23
|
+
// WHY THE NESTED-DISPATCH PARAGRAPH EXISTS (2026-07-31): this hook fires at EVERY depth, but the
|
|
24
|
+
// frizz worker contract reaches only the ROOT worker — so its "keep fan-out shallow / a rested agent
|
|
25
|
+
// is not reliably re-woken by grandchildren" rule was delivered exclusively to the one agent that
|
|
26
|
+
// does not spawn grandchildren, and withheld from the depth-1 child that does. Into that silence a
|
|
27
|
+
// user-level CLAUDE.md ("the parent stays awake and polls the child's transcript") supplied a
|
|
28
|
+
// hand-rolled polling recipe, and it failed: the `.output` path is a SYMLINK, so `stat` without -L
|
|
29
|
+
// returns the LINK's size (= the length of its target path) and its frozen creation mtime, while
|
|
30
|
+
// the `"type":"result"` record is never reliably written. Both halves of the predicate
|
|
31
|
+
// false-negatived at once, a live 413KB helper read as "size=153 age=325s results=0", and the
|
|
32
|
+
// dispatcher discarded it and redid the work itself. This paragraph is the only place that reaches
|
|
33
|
+
// a nested dispatcher without depending on its parent remembering to restate the norm.
|
|
34
|
+
//
|
|
35
|
+
// GATE: inert unless FRIZZ_THREAD is set (not a frizz worker → allow every dispatch unmodified).
|
|
36
|
+
//
|
|
37
|
+
// DROPPED vs cc's agent-dispatch.mjs (see DECISIONS.md): the `.dispatch-count` bump (it only gates
|
|
38
|
+
// cc's SubagentStop rest-recorder, which cc-worker does not ship) and the THREAD:-ledger write +
|
|
39
|
+
// thread-existence DENY gate (that guards the orchestrator's "file the thread before dispatching"
|
|
40
|
+
// discipline; a worker owns exactly one already-existing thread and its helpers own no thread).
|
|
41
|
+
//
|
|
42
|
+
// FAIL OPEN: any parse error → allow unmodified. A broken dispatch hook must never halt work.
|
|
43
|
+
import { readFileSync } from 'node:fs';
|
|
44
|
+
|
|
45
|
+
const EPILOGUE = `
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
[ORCHESTRATION EPILOGUE — auto-appended by the frizz worker dispatch hook] You are a helper sub-agent for a frizz worker. Your final message is the handoff: report your outcome/status; what you did; changed files, artifacts, and commit SHA when applicable; verification or evidence and its result; caveats or unresolved work; and the next action when one remains. A bare "done" or progress-only message is not a complete handoff.
|
|
49
|
+
The worker's scratchpad is shared: merge your own scoped progress into it as you work rather than leaving the root as its sole writer. Re-read before every edit and preserve every other agent's content. This exact scratchpad is Frizz coordination state, not a project deliverable or source edit: a scoped merge is an explicit exception to delegated phrases such as "write only <path>" or "do not modify the repo"; never classify it as unauthorized or roll it back. Never delete, truncate, reinitialize, move, or replace the whole file. Other project files, including repository-root files, remain governed by your delegated authority; their location alone neither permits nor forbids editing. Do not edit other \`.frizz/\` state unless your prompt explicitly asks you to.
|
|
50
|
+
You have an upward channel while you work: \`SendMessage({to: "main", summary: "<5-10 words>", message: "…"})\` delivers to your dispatcher. Use it when the dispatcher acting before you finish could change the outcome—for example, when you hit an unresolved blocker, complete a milestone another task needs, or discover that your instructions should change. Do not use it for routine progress updates.
|
|
51
|
+
Do the work yourself: do NOT dispatch sub-agents of your own unless your dispatch prompt explicitly tells you to. You are already one prong of someone else's fan-out, and another layer below you buys little — it splits the context you were handed, buries the real work one level further from whoever reads the tree, and leaves you collecting a handoff instead of doing the task. A slice that feels large is still yours to work through in your own turn.
|
|
52
|
+
If your prompt DOES ask you to dispatch a helper, its completion is delivered to you automatically. Never hand-roll a wait loop over a helper's transcript or \`.output\` path to decide whether it finished: that path is a SYMLINK, so \`stat\` without \`-L\` reports the link's own size (the length of its target path, ~150 bytes) and its frozen creation mtime, and the \`"type":"result"\` record is not reliably written — so a helper that is working hard reads as tiny, stale, and dead, and you will discard live work and redo it. Judge a helper only by its completion notification or the text it returns. Give it a \`description\` naming its narrower slice rather than restating your own, so the dispatch tree stays readable.`;
|
|
53
|
+
|
|
54
|
+
/** @param {unknown} obj @returns {never} */
|
|
55
|
+
function emit(obj) {
|
|
56
|
+
process.stdout.write(JSON.stringify(obj));
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// WORKER GATE — inert outside a frizz worker session.
|
|
62
|
+
if (!(process.env.FRIZZ_THREAD ?? '').trim()) emit({});
|
|
63
|
+
|
|
64
|
+
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
65
|
+
const ti = input.tool_input ?? {};
|
|
66
|
+
|
|
67
|
+
if (ti.run_in_background !== true) {
|
|
68
|
+
emit({
|
|
69
|
+
hookSpecificOutput: {
|
|
70
|
+
hookEventName: 'PreToolUse',
|
|
71
|
+
permissionDecision: 'deny',
|
|
72
|
+
permissionDecisionReason:
|
|
73
|
+
'frizz worker (hook-enforced): Agent sub-agents MUST be dispatched with run_in_background:true — never foreground/blocking. A foreground agent blocks the worker turn and a human interjection orphans its work. Re-send this Agent call with run_in_background:true.',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Strip name/team_name (they strand nested dispatches), then append the epilogue once.
|
|
79
|
+
const { name: _droppedName, team_name: _droppedTeam, ...tiStripped } = ti;
|
|
80
|
+
// Idempotence is "this prompt ALREADY ENDS WITH the epilogue", not "this prompt mentions the
|
|
81
|
+
// marker anywhere". A substring test silently ate the epilogue for any prompt that merely QUOTED
|
|
82
|
+
// the marker — e.g. a worker asking a helper to report whether the epilogue reached it, which is
|
|
83
|
+
// exactly how this was caught. endsWith still catches a genuine double-fire (the only real case).
|
|
84
|
+
const prompt = typeof ti.prompt === 'string' ? ti.prompt : '';
|
|
85
|
+
const updatedInput = prompt.endsWith(EPILOGUE)
|
|
86
|
+
? tiStripped
|
|
87
|
+
: { ...tiStripped, prompt: prompt + EPILOGUE };
|
|
88
|
+
|
|
89
|
+
emit({
|
|
90
|
+
hookSpecificOutput: {
|
|
91
|
+
hookEventName: 'PreToolUse',
|
|
92
|
+
permissionDecision: 'allow',
|
|
93
|
+
updatedInput,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
emit({}); // fail open — allow unmodified
|
|
98
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function hasEscapingBackgroundJob(raw: unknown): boolean
|
|
2
|
+
export function evaluateBashBackgroundHook(
|
|
3
|
+
input: unknown,
|
|
4
|
+
env?: Record<string, string | undefined>,
|
|
5
|
+
): Record<string, unknown>
|
|
6
|
+
export function isDirectHookExecution(argv1: unknown, moduleUrl: string): boolean
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// PreToolUse hook on `Bash` (frizz-worker). Claude's native `run_in_background` flag registers a
|
|
4
|
+
// task, output file, terminal notification, and wake. Shell job control (`cmd &`) does none of those:
|
|
5
|
+
// the child can survive after the Bash tool returns, but Claude and frizz have no lifecycle identity
|
|
6
|
+
// for it. A worker can then rest forever waiting for a notification that cannot exist.
|
|
7
|
+
//
|
|
8
|
+
// Block only an ESCAPING local background job. Self-contained shell concurrency remains valid when
|
|
9
|
+
// the command explicitly waits for its children or owns them with an EXIT trap before Bash returns.
|
|
10
|
+
//
|
|
11
|
+
// GATE: inert unless FRIZZ_THREAD is set (ordinary Claude sessions keep their native behavior).
|
|
12
|
+
// FAIL OPEN: malformed hook input allows the command rather than wedging a worker.
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { basename } from 'node:path';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
|
|
17
|
+
/** @param {unknown} obj @returns {never} */
|
|
18
|
+
function emit(obj) {
|
|
19
|
+
process.stdout.write(JSON.stringify(obj));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Blank heredoc bodies while preserving line/character positions. A script being WRITTEN may contain
|
|
25
|
+
* `&`; only the shell currently executing this tool call is relevant to the lifecycle escape.
|
|
26
|
+
* @param {string} command
|
|
27
|
+
*/
|
|
28
|
+
function withoutHeredocBodies(command) {
|
|
29
|
+
const lines = command.split('\n');
|
|
30
|
+
/** @type {string[]} */
|
|
31
|
+
const pending = [];
|
|
32
|
+
let active;
|
|
33
|
+
return lines.map((line) => {
|
|
34
|
+
if (active !== undefined) {
|
|
35
|
+
if (line.trim() === active) {
|
|
36
|
+
active = pending.shift();
|
|
37
|
+
return line;
|
|
38
|
+
}
|
|
39
|
+
return ' '.repeat(line.length);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shell accepts quoted and bare heredoc delimiters. Multiple heredocs on one command line are
|
|
43
|
+
// consumed in declaration order.
|
|
44
|
+
for (const match of line.matchAll(/<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_-]*))/g)) {
|
|
45
|
+
const delimiter = match[1] ?? match[2] ?? match[3];
|
|
46
|
+
if (delimiter) pending.push(delimiter);
|
|
47
|
+
}
|
|
48
|
+
active = pending.shift();
|
|
49
|
+
return line;
|
|
50
|
+
}).join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replace quoted regions with spaces. Operators sent inside `ssh host '…'` are remote, and quoted
|
|
55
|
+
* prose such as `printf '&'` is not local shell job control. Backticks are also synchronous command
|
|
56
|
+
* substitutions from the outer shell's perspective.
|
|
57
|
+
* @param {string} command
|
|
58
|
+
*/
|
|
59
|
+
function withoutQuotedRegions(command) {
|
|
60
|
+
let quote = '';
|
|
61
|
+
let escaped = false;
|
|
62
|
+
let out = '';
|
|
63
|
+
for (let i = 0; i < command.length; i++) {
|
|
64
|
+
const c = command[i];
|
|
65
|
+
if (escaped) {
|
|
66
|
+
out += quote ? ' ' : c;
|
|
67
|
+
escaped = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === '\\' && quote !== "'") {
|
|
71
|
+
out += quote ? ' ' : c;
|
|
72
|
+
escaped = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (quote) {
|
|
76
|
+
// Double quotes may contain a complete command substitution with its OWN quote grammar:
|
|
77
|
+
// `"$(python -c 'print(f"{x&255}")')"`. Treating the f-string's double quote as the end of the
|
|
78
|
+
// outer region exposed arithmetic `&` as fake job control in a real corpus call. The outer shell
|
|
79
|
+
// waits for command substitution, so blank the balanced substitution as part of the quote.
|
|
80
|
+
if (quote === '"' && c === '$' && command[i + 1] === '(') {
|
|
81
|
+
out += ' ';
|
|
82
|
+
i += 2;
|
|
83
|
+
let depth = 1;
|
|
84
|
+
let innerQuote = '';
|
|
85
|
+
let innerEscaped = false;
|
|
86
|
+
for (; i < command.length; i++) {
|
|
87
|
+
const inner = command[i];
|
|
88
|
+
out += inner === '\n' ? '\n' : ' ';
|
|
89
|
+
if (innerEscaped) {
|
|
90
|
+
innerEscaped = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (inner === '\\' && innerQuote !== "'") {
|
|
94
|
+
innerEscaped = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (innerQuote) {
|
|
98
|
+
if (inner === innerQuote) innerQuote = '';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (inner === "'" || inner === '"' || inner === '`') {
|
|
102
|
+
innerQuote = inner;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inner === '(') depth++;
|
|
106
|
+
else if (inner === ')' && --depth === 0) break;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (c === quote) quote = '';
|
|
111
|
+
out += c === '\n' ? '\n' : ' ';
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
115
|
+
quote = c;
|
|
116
|
+
out += ' ';
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
out += c;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A LOCAL shell wrapper runs its script in THIS machine's process tree, so `bash -c 'job &'` escapes
|
|
125
|
+
// exactly as a bare `job &` does — the wrapper forks the job and exits. Blanking quoted regions is
|
|
126
|
+
// what exempts `ssh host '… &'` (that job is remote and the ssh client still waits), but it hid the
|
|
127
|
+
// local wrapper along with it, leaving a one-token bypass of the whole guard. So scan the sanitized
|
|
128
|
+
// text for a wrapper, then re-read its script from the ORIGINAL command and recurse.
|
|
129
|
+
//
|
|
130
|
+
// Only in COMMAND POSITION. A wrapper passed as an ARGUMENT belongs to whatever program precedes it,
|
|
131
|
+
// and that program decides where the script runs: `docker run img bash -c …` and
|
|
132
|
+
// `limactl shell vm bash -lc …` both appear in the real corpus, and neither backgrounds anything
|
|
133
|
+
// local. That is the same call the `ssh` exemption already makes. It costs the genuinely-local
|
|
134
|
+
// `xargs sh -c 'job &'`, which the corpus never shows, and under-blocking is the safer miss here.
|
|
135
|
+
const LOCAL_SHELL_SCRIPT = /(?:^|[\n;|&({])\s*(?:\/\S*\/)?(?:bash|sh|zsh|dash|ksh)(?:\s+-[A-Za-z]+)*\s+-[A-Za-z]*c(?=\s)/g;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read the quoted argument that begins at `from` in the original command, undoing only the escaping
|
|
139
|
+
* the surrounding quote itself performs.
|
|
140
|
+
* @param {string} raw @param {number} from
|
|
141
|
+
*/
|
|
142
|
+
function quotedArgumentAt(raw, from) {
|
|
143
|
+
let i = from;
|
|
144
|
+
while (raw[i] === ' ' || raw[i] === '\t') i++;
|
|
145
|
+
const quote = raw[i];
|
|
146
|
+
if (quote !== "'" && quote !== '"') return '';
|
|
147
|
+
let out = '';
|
|
148
|
+
for (i++; i < raw.length; i++) {
|
|
149
|
+
if (raw[i] === '\\' && quote === '"' && i + 1 < raw.length) {
|
|
150
|
+
out += raw[++i];
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (raw[i] === quote) return out;
|
|
154
|
+
out += raw[i];
|
|
155
|
+
}
|
|
156
|
+
return '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Return true when this Bash call starts a local background job and can return without joining or
|
|
161
|
+
* terminating it. This is deliberately a small shell-lifecycle recognizer, not a general parser.
|
|
162
|
+
* @param {unknown} raw
|
|
163
|
+
* @param {number} depth
|
|
164
|
+
*/
|
|
165
|
+
export function hasEscapingBackgroundJob(raw, depth = 0) {
|
|
166
|
+
if (typeof raw !== 'string' || !raw.trim()) return false;
|
|
167
|
+
const command = withoutQuotedRegions(withoutHeredocBodies(raw));
|
|
168
|
+
/** @type {number[]} */
|
|
169
|
+
const operators = [];
|
|
170
|
+
for (let i = 0; i < command.length; i++) {
|
|
171
|
+
if (command[i] !== '&') continue;
|
|
172
|
+
const before = command[i - 1] ?? '';
|
|
173
|
+
const after = command[i + 1] ?? '';
|
|
174
|
+
// `&&`, redirects (`2>&1`, `&>`), and an escaped literal are not background operators.
|
|
175
|
+
if (before === '&' || after === '&' || before === '>' || before === '<' || after === '>' || before === '\\') continue;
|
|
176
|
+
operators.push(i);
|
|
177
|
+
}
|
|
178
|
+
if (operators.length > 0) {
|
|
179
|
+
// A lifecycle action AFTER the last launch makes the command self-contained. `kill` alone does
|
|
180
|
+
// not: the signal is asynchronous, so the shell still needs a wait before returning. An EXIT trap
|
|
181
|
+
// owns cleanup at the shell boundary.
|
|
182
|
+
const tail = command.slice(operators[operators.length - 1] + 1);
|
|
183
|
+
if (!(/\bwait\b/.test(tail) || /\btrap\b[^\n;]*\b(?:EXIT|0)\b/.test(tail))) return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Both sanitizers substitute character-for-character, so a match position in the sanitized text
|
|
187
|
+
// still indexes the original. A length mismatch would mean that invariant broke: fail open rather
|
|
188
|
+
// than slice the wrong bytes. The depth cap bounds `sh -c 'sh -c …'` nesting.
|
|
189
|
+
if (depth >= 3 || command.length !== raw.length) return false;
|
|
190
|
+
// `matchAll` iterates a CLONE, so the module-level regex keeps no cursor for a nested call to reset
|
|
191
|
+
// out from under this loop — `exec` on the shared object spins forever on `bash -c "sh -c '…'"`.
|
|
192
|
+
for (const match of command.matchAll(LOCAL_SHELL_SCRIPT)) {
|
|
193
|
+
const script = quotedArgumentAt(raw, match.index + match[0].length);
|
|
194
|
+
if (hasEscapingBackgroundJob(script, depth + 1)) return true;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function evaluateBashBackgroundHook(input, env = process.env) {
|
|
200
|
+
if (!String(env.FRIZZ_THREAD ?? '').trim()) return {};
|
|
201
|
+
const command = input && typeof input === 'object'
|
|
202
|
+
? String(input.tool_input?.command ?? '')
|
|
203
|
+
: '';
|
|
204
|
+
if (!hasEscapingBackgroundJob(command)) return {};
|
|
205
|
+
const codex = typeof input?.model === 'string';
|
|
206
|
+
return {
|
|
207
|
+
hookSpecificOutput: {
|
|
208
|
+
hookEventName: 'PreToolUse',
|
|
209
|
+
permissionDecision: 'deny',
|
|
210
|
+
permissionDecisionReason:
|
|
211
|
+
codex
|
|
212
|
+
? 'Frizz blocked an untracked shell background job (`&`). Shell job control can return without a Codex lifecycle handle, so Frizz cannot report completion or wake this agent. For work that must continue while you do something else, remove `&` and use the managed unified exec pattern: start `tools.exec_command(...)`, call `yield_control()`, then await and fully drain that same process. A returned `session_id` alone is only foreground continuation. For bounded parallel work inside one shell call, finish with `wait` (after `kill`, if used) or own cleanup with an EXIT trap.'
|
|
213
|
+
: 'Frizz blocked an untracked shell background job (`&`). Shell job control can return from Bash without a Claude task ID, so Frizz cannot report completion or wake this agent. For a long-running local command, remove `&` and call Bash with `run_in_background:true`. For bounded parallel work inside one Bash call, finish with `wait` (after `kill`, if used) or own cleanup with an EXIT trap.',
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function isDirectHookExecution(argv1, moduleUrl) {
|
|
219
|
+
return typeof argv1 === 'string'
|
|
220
|
+
&& basename(argv1) === 'bash-background.mjs'
|
|
221
|
+
&& pathToFileURL(argv1).href === moduleUrl;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The server imports `hasEscapingBackgroundJob` and its production build bundles this module into
|
|
225
|
+
// `src/index.js`. esbuild rewrites `import.meta.url` to that bundle URL, so URL equality alone would
|
|
226
|
+
// mistake the whole server for this executable and block startup reading hook JSON from stdin.
|
|
227
|
+
if (isDirectHookExecution(process.argv[1], import.meta.url)) {
|
|
228
|
+
try {
|
|
229
|
+
const env = process.argv.includes('--frizz-thread')
|
|
230
|
+
? { ...process.env, FRIZZ_THREAD: process.env.FRIZZ_THREAD || 'codex-worker' }
|
|
231
|
+
: process.env;
|
|
232
|
+
emit(evaluateBashBackgroundHook(JSON.parse(readFileSync(0, 'utf8')), env));
|
|
233
|
+
} catch {
|
|
234
|
+
emit({});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// PreToolUse hook on AskUserQuestion (frizz-worker). A frizz worker runs under a dashboard, not a
|
|
4
|
+
// live chat: an interactive question prompt would hang the session invisibly (nobody is at the
|
|
5
|
+
// keyboard to click it). Deny with a redirect to the async pattern: ask in the FINAL MESSAGE via one
|
|
6
|
+
// or more ```question fenced blocks, then come to rest; answers arrive as the next user message.
|
|
7
|
+
// GATE: inert unless FRIZZ_THREAD is set. FAIL OPEN on parse errors.
|
|
8
|
+
//
|
|
9
|
+
// SECOND GATE: also inert when FRIZZ_NATIVE_ASK=1. The premise above — "nobody is at the keyboard" —
|
|
10
|
+
// is only true where the question has nowhere to go. On the Claude session-broker path frizz now
|
|
11
|
+
// intercepts the call at canUseTool and renders it as a real question card on the dashboard, which the
|
|
12
|
+
// operator answers and which returns the chosen labels to the tool. The broker bridge sets that var
|
|
13
|
+
// only when an InteractionStore is actually wired to render and resolve the card, so a denial here
|
|
14
|
+
// would block a question the operator CAN see. The tmux path never sets it (and additionally drops the
|
|
15
|
+
// tool with --disallowedTools), so it keeps the deny plus the ```question redirect.
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const slug = process.env.FRIZZ_THREAD;
|
|
19
|
+
if (!slug) process.exit(0);
|
|
20
|
+
if (process.env.FRIZZ_NATIVE_ASK === '1') process.exit(0);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
JSON.parse(readFileSync(0, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
process.exit(0); // fail open — a broken hook must never halt work
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process.stdout.write(
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
hookSpecificOutput: {
|
|
31
|
+
hookEventName: 'PreToolUse',
|
|
32
|
+
permissionDecision: 'deny',
|
|
33
|
+
permissionDecisionReason:
|
|
34
|
+
'Interactive prompts freeze headless workers (no one is at the keyboard to answer). Ask in your FINAL MESSAGE instead, using one or more ```question fenced blocks — each self-contained (context + the specific question + lettered `- A. …` options + a Recommendation); the frizz Queue renders each as a card and the human replies "A"/"2"/prose in the composer. A ```question block IS the handback: write it and END YOUR TURN (do NOT also add a done/awaiting fence, and do NOT invoke this tool again) — the human answers from the queue.',
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
process.exit(0);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// PermissionRequest hook on ExitPlanMode (frizz) — a frizz worker runs under a dashboard, not a
|
|
4
|
+
// live chat, so the plan-APPROVAL prompt (shown when the model calls ExitPlanMode to present a plan
|
|
5
|
+
// and ask to proceed) would hang the session invisibly. Deny it and redirect the worker to encode
|
|
6
|
+
// its plan/ask in the thread file instead.
|
|
7
|
+
//
|
|
8
|
+
// WHY PermissionRequest, not PreToolUse: ExitPlanMode is a PERMISSION surface, not a plain tool —
|
|
9
|
+
// it is denied via PermissionRequest (AskUserQuestion, a real tool, is denied via PreToolUse in the
|
|
10
|
+
// sibling deny-ask.mjs). CAVEAT: PermissionRequest hooks do NOT fire under `claude -p` (headless
|
|
11
|
+
// print mode); frizz workers run as INTERACTIVE tmux `claude` sessions, where they DO fire. So
|
|
12
|
+
// this protects the real worker path; the `-p` smoke test cannot exercise it (documented).
|
|
13
|
+
//
|
|
14
|
+
// PLAN-MODE SOFTLOCK — why this denies UNCONDITIONALLY when gated: a session genuinely IN plan mode
|
|
15
|
+
// is read-only until ExitPlanMode is approved, so denying ExitPlanMode there would SOFTLOCK it
|
|
16
|
+
// (can't exit → can't edit → can't even follow the "write to the thread" redirect). This hook does not
|
|
17
|
+
// branch on the session's mode. (An earlier note here claimed the PermissionRequest input carries no
|
|
18
|
+
// permission-mode signal — that is FALSE and was corrected 2026-07-25: the payload carries
|
|
19
|
+
// `permission_mode`, `tool_name`, and `tool_input`, which is exactly what the sibling perm-policy.mjs
|
|
20
|
+
// keys its rules on. The unconditional deny is kept because the SOURCE fix below makes plan mode
|
|
21
|
+
// unreachable for a frizz worker anyway, not because the signal is unavailable.) The fix is at the SOURCE
|
|
22
|
+
// instead — frizz NEVER spawns a worker in plan mode (dispatch.ts coerces `--permission-mode plan`
|
|
23
|
+
// → `auto` in both command builders), so a real frizz worker is never in plan mode and this deny
|
|
24
|
+
// only ever meets a SPURIOUS ExitPlanMode call (nothing to exit → deny + redirect is correct). A
|
|
25
|
+
// worker "plans" by writing a plan file (`.frizz/plans/<topic>.md`) and asking via a ```question
|
|
26
|
+
// approval block, never via interactive plan mode.
|
|
27
|
+
// RESIDUAL GAP (accepted, documented): the deny could softlock only a FOREIGN session that is
|
|
28
|
+
// simultaneously in plan mode AND running with FRIZZ_THREAD set AND this plugin loaded — a combo
|
|
29
|
+
// frizz never produces (FRIZZ_THREAD is set only by frizz dispatch, which coerces plan away).
|
|
30
|
+
// A normal plan-mode session outside frizz is untouched (this hook is inert without FRIZZ_THREAD).
|
|
31
|
+
//
|
|
32
|
+
// GATE: inert unless FRIZZ_THREAD is set. FAIL OPEN on any parse error — a broken hook must never
|
|
33
|
+
// halt work.
|
|
34
|
+
import { readFileSync } from 'node:fs';
|
|
35
|
+
|
|
36
|
+
const slug = process.env.FRIZZ_THREAD;
|
|
37
|
+
if (!slug) process.exit(0);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
JSON.parse(readFileSync(0, 'utf8'));
|
|
41
|
+
} catch {
|
|
42
|
+
process.exit(0); // fail open — a broken hook must never halt work
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The instructive redirect rides TOP-LEVEL `additionalContext` (per the Claude Code hooks docs) —
|
|
46
|
+
// on a PermissionRequest DENY the `decision` object carries ONLY `{behavior:"deny"}`; the reason
|
|
47
|
+
// the model reads is `additionalContext`, injected as a plain-text system-reminder. Exit 0 with
|
|
48
|
+
// this JSON on stdout (exit 2 would make Claude Code ignore the JSON — never mix).
|
|
49
|
+
const reason =
|
|
50
|
+
'Interactive plan-approval prompts freeze headless workers (no one is at the keyboard to approve). Do NOT present a plan for approval. Instead: if the plan is settled, just proceed with the work. If the plan is the deliverable, write it into a plan file `.frizz/plans/<topic>.md` (free-form markdown) and/or your scratchpad. If it needs a human call before you build, ask in your FINAL MESSAGE with a two-option ```question block stating what you need approved, then come to rest — the human reviews it from the frizz queue.';
|
|
51
|
+
|
|
52
|
+
process.stdout.write(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
hookSpecificOutput: {
|
|
55
|
+
hookEventName: 'PermissionRequest',
|
|
56
|
+
decision: { behavior: 'deny' },
|
|
57
|
+
},
|
|
58
|
+
additionalContext: reason,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
process.exit(0);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/bash-background.mjs\"" }
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"matcher": "Agent",
|
|
12
|
+
"hooks": [
|
|
13
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/agent-dispatch.mjs\"" }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"matcher": "AskUserQuestion",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/deny-ask.mjs\"" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PostToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Agent",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/agent-bind.mjs\"" }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"hooks": [
|
|
32
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=nudge --event=PostToolUse" }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"UserPromptSubmit": [
|
|
37
|
+
{
|
|
38
|
+
"hooks": [
|
|
39
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=nudge" }
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"Stop": [
|
|
44
|
+
{
|
|
45
|
+
"hooks": [
|
|
46
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad-stop.mjs\"" }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"PermissionRequest": [
|
|
51
|
+
{
|
|
52
|
+
"matcher": "ExitPlanMode",
|
|
53
|
+
"hooks": [
|
|
54
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/deny-plan.mjs\"" }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"matcher": "*",
|
|
59
|
+
"hooks": [
|
|
60
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/perm-policy.mjs\"" }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"PreCompact": [
|
|
65
|
+
{
|
|
66
|
+
"matcher": "auto",
|
|
67
|
+
"hooks": [
|
|
68
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/precompact-instructions.mjs\"" },
|
|
69
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=precompact" }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"matcher": "manual",
|
|
74
|
+
"hooks": [
|
|
75
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/precompact-instructions.mjs\"" },
|
|
76
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=precompact" }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"SessionStart": [
|
|
81
|
+
{
|
|
82
|
+
"matcher": "startup",
|
|
83
|
+
"hooks": [
|
|
84
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-seed.mjs\"" },
|
|
85
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=session-start" }
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"matcher": "resume",
|
|
90
|
+
"hooks": [
|
|
91
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-seed.mjs\"" },
|
|
92
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=session-start" }
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"matcher": "clear",
|
|
97
|
+
"hooks": [
|
|
98
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-seed.mjs\"" },
|
|
99
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=session-start" }
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"matcher": "compact",
|
|
104
|
+
"hooks": [
|
|
105
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-seed.mjs\"" },
|
|
106
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scratchpad.mjs\" --mode=session-start" }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|