What:README.md、references/behaviors.md 與 skills/hooks-install/SKILL.md 同步這一批的接線事實。 Why: - 文件寫著「這些 CLI 沒有 pre-tool hook」。那句話是錯的,也正是三支 CLI 長期沒有守門的原因。照著它報告,會讓使用者以為只有 claude 有保護、其餘四支本來就接不上。 - kiro 的 degraded 原本被寫成接線缺東西,實際上是那支 CLI 擋不下技能叫用。兩件事的處理方式完全不同。 How: - 一支 CLI 一列,列出接線位置、事件與 matcher、阻擋形態與判定,並寫明 claude、codex、copilot、antigravity 回 wired,kiro 回 degraded。 - 新增驗證等級表,把「形狀」與「觸發」分開記:形狀是那支 CLI 真的讀得懂設定,觸發是 hook 真的被叫用過。copilot 的形狀未證、antigravity 與 codex 的觸發未驗證,一律照實列出,不含糊帶過。 - README 補上 hooks/skill-name.sh 與 hooks/deny.sh 兩列,並補 COPILOT_HOME、KIRO_HOME、JSC_ANTIGRAVITY_HOOKS、JSC_COPILOT_INSTRUCTIONS、JSC_ANTIGRAVITY_RULES 幾個環境變數。 - kiro 的接線位置從 .kiro/hooks/jsc-hooks.json 全面改寫成 ~/.kiro/agents/jsc.json。 - 講明 write-guard.sh 三個模式與 SDLC 模型鎖在另外三支上是「還沒接」,不是「沒有 hook 可接」。 - behaviors.md 的完成條件與可驗證跡象跟著改,repair 那一列加上「技能名解析改 skill-name.sh、阻擋形態改 deny.sh,兩支是唯一真實來源」。 Who:codex、copilot、antigravity、kiro 四支 CLI 的 pre-tool hook 接線修正。
27 KiB
name, description
| name | description |
|---|---|
| hooks-install | Wire jsc hooks (STE100 guard, session timer, skill usage logger, SDLC model gate, plugin version guard, post-deploy restart gate, comment scope scanner, language guard, write and commit guard) into every installed AI CLI, purging all pre-existing hooks first — third-party ones included, backed up before removal. Drive it per CLI through tools/wire-cli.sh purge, tools/wire-cli.sh, tools/wire-cli.sh status, tools/wire-cli.sh smoke and tools/scan-hook-errors.sh. Hand any hook error, wiring or runtime, to jsc-hooks:repair, which must finish with a PR against develop; aborting the rest of the install to start that repair is allowed. Use after installing or updating the jsc plugin set; not for writing new hooks. |
hooks-install — wire jsc hooks into every installed CLI
Goal: make the nine hooks (ste100-guard.sh, session-timer.sh, skill-usage.sh, sdlc-gate.sh, version-guard.sh, restart-gate.sh, comment-scope.sh, lang-guard.sh, write-guard.sh) effective in every CLI, with nothing else wired alongside them.
Install on a clean slate. Every CLI is purged of all hooks first, third-party ones included, so a later failure has exactly one owner. tools/wire-cli.sh purge backs up every file it touches before it removes anything, so the removal stays reversible.
The wiring commands stored in user config use $JSC_HOME/current/jsc-hooks, not the versioned plugin cache path and not the development checkout. tools/wire-cli.sh {cli} creates or refreshes that symlink before it writes notify, shell aliases or Kiro hook JSON, then verifies the linked scripts exist. If the filesystem cannot create the symlink, the script must say so and explicitly fall back to the current root; it must never write a silent broken path. The bundled hooks/hooks.json follows the same rule: use ${CLAUDE_PLUGIN_ROOT} only where the host provides it, and fall back to $JSC_HOME/current/jsc-hooks for any other CLI reading the same manifest, so an unset Claude-only variable never expands into /hooks/....
Four of the five CLIs have a pre-tool hook that can block. The version guard and the restart gate reach codex, copilot and antigravity too — each at its own wiring point, with its own matcher and its own blocking shape. Never say a CLI "has no pre-tool hook"; that claim is wrong and it is what left three CLIs unguarded.
| CLI | Wiring point | Event and matcher | Blocking shape | Verdict |
|---|---|---|---|---|
| claude | hooks/hooks.json |
PreToolUse, matcher Skill |
stderr plus exit 2 | wired |
| codex | hooks/codex-hooks.json, pointed at by the hooks path string in .codex-plugin/plugin.json |
PreToolUse, matcher Bash |
stderr plus exit 2 | wired |
| copilot | hooks key of ~/.copilot/settings.json, merged in place |
PreToolUse, matcher skill |
stderr plus exit 2 | wired |
| antigravity | jsc block of ~/.gemini/config/hooks.json |
PreToolUse, matcher ^view_file$ (Grouped), plus PreInvocation (Flat) |
{"decision":"deny",...} on stdout |
wired |
| kiro | hooks key of ~/.kiro/agents/jsc.json, plus chat.defaultAgent=jsc |
agentSpawn, userPromptSubmit, stop |
warning injected on stdout; blocks nothing | degraded |
On the four non-claude CLIs every wired command carries its own CLI code as a JSC_CLI={code} prefix. A gate has to know which CLI it is running under before it can read a skill name out of skill-name.sh or a blocking shape out of deny.sh; with no code the skill name resolves to nothing and both gates pass in silence — config correct, matcher correct, blocks never. On antigravity it is worse: an unknown code makes deny.sh fall back to the exit-code shape, which that CLI ignores, so the gate decides to block and the CLI never hears it. The jsc-wrap.sh alias exports JSC_CLI as well, but only when the user starts the CLI through the alias from an interactive shell, so wiring never leans on it. wire-cli.sh asserts the prefix per CLI at wiring time, at status time and in smoke.
Only claude reaches all nine hooks. On codex, copilot and antigravity the SDLC gate still degrades to the skill-step check, the three write and commit guard modes are still unwired, and the comment and language scans still run as sweep — say exactly that, in the words the script prints, instead of implying full coverage.
kiro is degraded because the CLI cannot block a skill call, not because the wiring is short of anything. A skill there is a ResolveSkill request inside the agent, off the tool pipeline, so preToolUse never sees it and a non-zero exit from userPromptSubmit does not stop the turn. Injecting a warning on stdout is the only intervention left. Its hook declarations live in the agent config's hooks key — .kiro/hooks/ is not in kiro's config-directory constants and is never read — and that same agent file needs the two-level skill:// glob in resources (the default glob scans one level, jsc skills sit at jsc-{domain}/{name}/SKILL.md) plus an explicit tools list, with chat.defaultAgent set to jsc so the agent is chosen at all.
Shape is part of the wiring, and a wrong shape fails silently. Two rules are read straight out of the CLIs' own embedded specs and asserted by wire-cli.sh. Antigravity splits its events: PreToolUse and PostToolUse are Grouped — handlers wrapped in a matcher plus hooks group — while PreInvocation, PostInvocation and Stop are Flat. A Flat PreToolUse is discarded whole: the hook name still registers, no actions key is even generated, nothing errors, and the file reads as correct. Codex's plugin manifest takes hooks as a path string, exactly like skills; an inline object does not parse. That path is an override, so hooks/codex-hooks.json is derived from hooks/hooks.json — copied whole, with "matcher": "Skill" rewritten to "matcher": "Bash" — which keeps every other event and keeps hooks/hooks.json the single source of truth. Never hand-write the second file.
Copilot keeps hook config in the hooks key of settings.json — inline definitions keyed by event name. $COPILOT_HOME/hooks/ holds the scripts a hook runs, not the config; a config file written there sits on disk, correct and unread. That same settings.json also carries enabledPlugins and extraKnownMarketplaces, so wiring merges and never overwrites: back up first, touch only jsc's own entries under hooks, then read back and compare the top-level keys and every foreign hook entry against what was there before, restoring the backup if either moved. purge takes out only jsc's entries and leaves the third-party SessionStart alone.
Kiro declares hooks in the agent config's hooks key, and its only legal events are agentSpawn, userPromptSubmit, preToolUse, postToolUse and stop; the fields are command (required), matcher and timeout_ms — there is no on, run or env. The old wiring put on/run/env at the top level, kiro-cli agent validate reported nothing at all, and the file did nothing: unknown top-level keys are ignored in silence. A valid file is not a wired file, so check the shape separately.
kiro-cli agent validate always exits 0. Valid, illegal event name, on/run inside hooks, missing command — all four exit 0, and the errors only appear in the output. Reading the exit code builds a check that can never fail, which is the same class of bug as the ones being fixed here. Judge by the output: empty means valid. Output about something else — not logged in, expired credentials — means the check could not run, not that the file is bad; pass it and say so, because failing there would block wiring on every machine that is not logged in.
Verification level, per CLI. "Shape" means the CLI really parses the config; "firing" means a hook really ran. Keep them apart and report them as this table has them:
| CLI | Shape | Firing |
|---|---|---|
| claude | proven | proven |
| codex | proven — [hooks.state] in ~/.codex/config.toml records {event}:{group}:{entry}, a two-level index that only a Grouped structure produces; the manifest's hooks is a path string per the binary's own plugin-json-spec.md |
unverified |
| antigravity | proven — after wiring, agy -p "/hooks" lists all four entries with matcher=^view_file$, third-party block intact |
unverified (conversation quota exhausted) |
| copilot | unproven — settings.json has no read-only listing path; the location and entry form come from copilot help config and the working third-party entry already on the machine |
unverified |
| kiro | proven — kiro-cli agent validate passes with empty output, and a counter-check confirms it discriminates: sessionStart, on/run inside hooks, and a missing command each produce an error |
partly proven — agentSpawn and userPromptSubmit were observed firing; preToolUse and stop are unverified (model quota) |
Two more open items: whether kiro's two-level resources glob actually fixes skill visibility is unverified, and on the four non-claude CLIs the three write-guard.sh modes and the SDLC model lock are still unwired — not yet done, rather than failing. wire-cli.sh smoke asserts wiring content and script logic line by line; firing is outside its reach.
comment-scope.sh and lang-guard.sh both reach all five, wired at the same set of places, but on a different event and at a different moment each. Report the timing per CLI; never state it as one uniform behaviour:
| CLI | Scanning moment | Wired through |
|---|---|---|
| claude | Per file, the instant it is written | PostToolUse |
| codex | End of every turn, over the whole git worktree | notify in config.toml |
| kiro | On every prompt submit, over the whole git worktree — it sees what the previous turn wrote | userPromptSubmit in ~/.kiro/agents/jsc.json |
| copilot, antigravity | Once, when the session ends | tools/jsc-wrap.sh teardown |
The table above holds for both scanners. The sweep mode reads git diff HEAD, so its coverage matches what claude sees; only the feedback delay differs. Outside a git worktree sweep exits 0 in silence and nothing is scanned at all — say so when the user works outside git. Both prompt rule reminders still go into every rule file alongside the STE100 block, because a warning that arrives a turn late is worth less than not writing the offending text in the first place.
The lock file still works on those four because the SDLC skills call sdlc-gate.sh lock {stage} directly — that call is where the capability-tag comparison happens, so the gate keeps its force even where the prompt hook cannot be wired.
The gate needs $JSC_HOME/model-tags.tsv; when it is missing, report that jsc-cli:models (or jsc-cli/tools/model-tags.sh sync) must run once, because sdlc-gate.sh lock refuses to lock without it.
Treat any hook error as repair work, whether it appeared while wiring or while running. Stopping the remaining installs to start that repair is the right call; leaving a broken hook wired is not.
The detailed flow MUST run as a sub agent; the main agent only reports the summary.
Steps
-
Take the CLI list from the caller when it hands one over —
jsc-cli:deploypasses the list it already detected, and probing the same five executables a second time buys nothing. Runjsc-cli/tools/detect-clis.shyourself only when no list came in; that fallback is what keeps this skill usable when it is called on its own. The script always exits 0 and prints onename<TAB>path<TAB>versionline per installed CLI. Done when you hold that list and have said which of the two ways produced it; when it is empty, report that no CLI was detected and stop. -
Run the five-stage pipeline purge → wire → status → smoke → scan once per detected CLI. Run the first CLI's pipeline on its own, because
tools/wire-cli.sh {cli}is what refreshes the shared$JSC_HOME/current/jsc-hookslink and two CLIs must not rewrite it at the same time; once that first pipeline has finished, run every remaining CLI's pipeline in parallel, one sub agent per CLI — the five stages of one CLI stay in this order, but different CLIs touch different config files and share nothing else. Every stage prints its verdict on its first line, so read that line and never infer the outcome from the prose below it.tools/wire-cli.sh purge {cli}— backs up every file it touches, removes all hooks, re-reads each file to confirm the removal, and restores the backup by itself when a check fails. Marker matching trims leading and trailing whitespace, so an indented or padded marker block is still removed as the same jsc-owned block. Exit 0 ispurged, exit 3 isskipped(that CLI's executable is not on this machine, so skip its remaining stages too), exit 4 isfailedand goes to step 3. Exit 2 is a bad CLI name, not a purge outcome — fix the name and rerun the stage.tools/wire-cli.sh {cli}— owns both the wiring and its verification: it refreshes the link, writes the config, alias or hook file inside a<!-- jsc-hooks -->(or# jsc-hooks) marker block, re-reads every file it wrote, confirms the block is present and correctly placed, and confirms the stored runtime paths resolve to existing scripts before it prints a success status. Exit 0 iswiredand is the expected result on claude, codex, copilot and antigravity; exit 1 isdegradedand is expected on kiro alone; exit 3 isskipped, exit 4 isfailedand goes to step 3. Exit 2 is a bad CLI name — fix the name and rerun. The matcher is verified on its own, not just the presence of a key: a key that is there with the wrong matcher reports as wired and fires never.tools/wire-cli.sh status {cli}— the read-only inventory of what the previous stage wrote. It writes nothing and runs no hook, so it is safe to run right after wiring. Exit 0 iswired(claude, codex, copilot, antigravity), exit 1 isdegraded(kiro), exit 3 isskipped, exit 5 isunwired, which names every missing item and means the wiring stage has to run again before you continue. Exit 2 is a bad CLI name. For codex this stage is the only one that reads the installedjsc-hooksmanifest in the Codex plugin cache and reports a staleUserPromptSubmitcommand there, the one that expands${CLAUDE_PLUGIN_ROOT}into/hooks/...; carry that item into the report.tools/wire-cli.sh smoke {cli}— runs every wired mode of all nine hooks once, plus each decision path of the work-package check, of the restart gate and of the write and commit guard. It catches what the wiring check cannot see: a hook that is wired correctly and still fails when it executes. It also runs each CLI's real payload throughskill-name.sh, each blocking shape throughdeny.sh, and those same payloads straight throughrestart-gate.shend to end, so a break anywhere along parse, decide and emit is caught — the two ends look healthy on their own while the middle silently passes everything through, which is exactly how three CLIs went unguarded. It prints its own result-line count aslines<TAB>{count}and asserts that count against what it expected to run, so read the number from that line and never restate a number of your own. Exit 0 isok, exit 4 isfailed— either a hook errored or the line count did not match, and both go to step 3. Exit 2 is a bad CLI name.tools/scan-hook-errors.sh --cli {cli}— only claude keeps hook results in its native records and can answercleanorerrors; codex, copilot, antigravity and kiro answerunavailable, and their runtime evidence comes from the smoke stage alone. Exit 0 covers bothcleanandunavailable, exit 1 iserrorsand every entry withjsc=truegoes to step 3, exit 2 is a bad CLI name.
Done when every detected CLI has exactly one verdict line per stage, no stage exited 2, the smoke stage's
linescount matches its own assertion, the four non-claude CLIs are reported asunavailablerather than clean on the scan stage, and antigravity and kiro carry the note that their hook firing is unverified. -
For each error — a failed purge, a failed wiring, an
unwiredstatus, a failed smoke, or a scanned error withjsc=true— runtools/report-error.sh --hook {script name} --exit {code} --summary "{reason}" --cli {cli}with the script's[jsc]output on stdin, then hand the failure tojsc-hooks:repair, which MUST run as a sub agent and must finish by opening a PR againstdevelop. Aborting the remaining installs here is allowed as long as the repair starts. Exit 0 with anERROR_{HASH}page name and URL on stdout means the page was written; exit 0 with empty output meansJSC_WIKI_REPO_ERRORandJSC_WIKI_REPOare both unset, so carry that reason into step 4 instead; exit 2 means the call itself was malformed —--hookor--summaryis missing — so fix the arguments and rerun the same call; exit 4 means the wiki record did not land, so report the failure text and still start the repair — a page that could not be written is no reason to leave a broken hook wired. Exit 4 covers two cases, and the report has to say which: a failed write, or the script refusing to write the error directory page because it could not read the old one back. That directory is appended to, never overwritten: every row on it is somebody else's error report, so the script reads the page, adds this run's row, and writes the whole page. Only a genuine 404 (wiki-getexit 4) means the page is not there yet and lets it build one from the template. An invalid key (exit 7) or any other API failure (exit 8) leaves the old rows unknown, so it skips the directory write and names the code instead — writing a fresh template over a directory it never read would erase every earlier report, with no merge and no backup behind it. A scanned error withjsc=falsebelongs to a third-party hook: report it and leave it alone. Skip this step when every CLI passed all five stages. Done when every error carries oneERROR_{HASH}result — a page name and URL, or the recorded reason no page was written — and one repair PR URL againstdevelop. -
Report five results per CLI — purge, wiring, status, smoke, scan — each with the reason its script printed, plus the smoke
linescount, anyERROR_{HASH}page name and every repair PR URL. Done when every detected CLI appears with one verdict per stage and every repair has a PR againstdevelop.
Notes
- Every hook script accepts both stdin JSON and environment variables (
JSC_CLI,JSC_SESSION_ID,JSC_SKILL,JSC_TOOL_NAME,JSC_TOOL_COMMAND,JSC_MODEL);jsc-wrap.shsets the first two itself. session-timer.shtakesstart(keep an existing start time),restart(always overwrite it, for a CLI with no session id — kiro),markandreport.wire-cli.shpicks the right one per CLI; do not hand-edit the generated hook files.startandrestartalso clear the restart gate whenever they decide this SessionStart is a new session, so the wiring of those two events is what lowers the gate after a restart — a CLI wired without them keeps the gate up until the user setsJSC_RESTART_GATE=off.hooks/skill-name.shis the one place that turns a CLI's hook payload into{domain}<TAB>{skill}, one subcommand per CLI: claude reads theskillfield, codex reads theSKILL.mdpath insidetool_input.command(it has no Skill tool — the model loads a skill by reading the file with Bash), copilot readstoolArgsand has to unwrap one layer of stringified JSON, antigravity readstoolCall.args.AbsolutePathand also the prompt text (a slash command injects the wholeSKILL.mdand produces no tool call), kiro reads the leading slash command inprompt. All five honourJSC_SKILLandSKILLfirst. It always exits 0: the gates fail open, and copilot's command hooks are fail-closed, where any non-zero exit means deny.hooks/deny.shis the matching single source for the blocking shape — stderr plus exit 2 for claude, codex and copilot; a single-line{"decision":"deny","reason":"..."}on stdout with a fixed exit 0 for antigravity, whose exit-code semantics are undocumented and must never be relied on; a printed warning and exit 0 for kiro, which cannot block. Neither guard keeps a second copy of either rule; a repair goes into these two files.restart-gate.shblocks jsc skill calls while$JSC_HOME/restart-required.d/{cli}exists — one file per CLI, named after the CLI code — so a freshly deployed skill set is not used by a process still running the old one. Each CLI reads only its own file: another CLI's file never blocks this one, and a restart clears only the file of the CLI that restarted.jsc-cli:deploywrites the current CLI's file throughrestart-gate.sh require {install|update} [{domain}...]at the end of an install or update;restart-gate.sh reportprints one line per file, so it is visible which CLIs still owe a restart. A leftover old-format single file at$JSC_HOME/restart-requiredblocks every CLI and is deleted on the nextclear— transitional only, andhooks/restart-gate.shrecords when it can be dropped. The gate matches skill names, not call chains, so a nested call to anything off the exemption list is blocked all the same;hooks/restart-gate.showns that list with a reason per entry, andjsc-meta/references/guidelines.md「部署後重啟閘門」carries the same list. Escape hatch:JSC_RESTART_GATE=off.write-guard.shtakes three blocking modes, wired on two PreToolUse matchers on claude only, so claude is still the only CLI where any of it takes effect — codex, copilot and antigravity now have a usable pre-tool hook, but these three modes are not wired there yet; say that, rather than blaming a missing hook, plus a fourth mode,release, that is wired nowhere and is called by a skill itself.stagereads the stage lock thatsdlc-gate.shalready owns and blocksWrite,EditandMultiEditwhileplanoranalyzeholds it, because those two stages produce wiki pages rather than files.reviewreads the current skill — the environment variable first, then the recordskill-usage.shkeeps — and blocks writes whilejsc-review:code-revieworjsc-review:api-docruns, since both only report findings. It deliberately does not blockjsc-review:comment-cleanup: that skill has to write, limited to comment lines, and deciding that limit needs per-language comment parsing of the whole proposed content, which would block legitimate cleanups more often than it caught bad ones — that boundary stays with the skill text and the later review.commitis wired onBashand blocks a single command that stages everything and commits in one go, plus any commit message carrying simplified characters or mojibake, which it decides by callinglang-guard.shrather than keeping a second word list. Agit add -Asplit across two separate tool calls is not caught, on purpose: catching it needs cross-call state that the blocked operator has no way to clear.releasedeletes that recorded skill and always exits 0;jsc-review:code-reviewandjsc-review:api-doccall it once each as they hand their findings back. It exists because the record says which skill was loaded last, not which one is still running: both audit skills end by leaving the fixing to their caller, and withoutreleaseevery write that caller makes stays blocked for the whole TTL, with the escape hatch or a wait as the only way out — a gate must never lock away its own release. Escape hatch:JSC_WRITE_GUARD=off, whichreleaseignores because clearing a record blocks nobody, plusJSC_WRITE_GUARD_TTLfor how long a recorded skill counts as still running.purgereaches the user-level config only. Hooks that another plugin ships in its ownhooks.jsonstay active, and uninstalling that plugin is the only way to clear them — say so when reporting, and treat their errors as third-party.- Backups land in
$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/, one directory per purge run, under the original file names. Hand that path to the user whenever a purge removed something. status claudereads Claude Code'sinstalled_plugins.jsonand checks theinstallPaththat the CLI actually loads. It must not check only thehooks.jsonnext to thewire-cli.shthat happens to be running, because a development checkout can otherwise hide a broken installed plugin.- Never set
JSC_READONLY=1for this skill.wire-cli.shrefusespurgeand wiring with exit 6 under that variable, which is exactly what a health check wants and exactly what an install must not have. smoketreatssdlc-gate.sh checkexit 2 as healthy: that exit is the stage lock blocking a turn on purpose, not a runtime error.comment-scope.sh,lang-guard.shandwrite-guard.shexit 2 count as healthy for the same reason — the check found something and said so. Their no-argument mode has no file name during smoke and exits 0 in silence;sweepdepends on the worktree it runs in, so it answers 2 whenever that worktree happens to carry an offending comment, a simplified character or a mojibake sequence, andwrite-guard.shanswers 2 whenever the machine happens to hold aplanstage lock or a recent audit skill. None of these is a broken hook.comment-scope.shtakes three modes:prompt(inject the rule summary at UserPromptSubmit), no argument at all (scan the file just written at PostToolUse, readingfile_pathfrom stdin JSON orJSC_CHANGED_FILE), andsweep [dir](scan every file the git worktree changed, for the four CLIs with no post-tool hook). All scanning modes read only the lines a diff added, skip markdown and binary files, and turn off entirely withJSC_COMMENT_SCOPE=off. The rule text itself lives in one place only,jsc-review'sreferences/comment-scope.md; never restate the list anywhere in this repo.lang-guard.shtakes the same three modes ascomment-scope.shand is wired at the same places, but it scans differently on purpose: it reads the whole file rather than comment lines only, and it does scan.mdand plain-text files, because those are exactly the non-code output the rule targets. It flags three things — simplified characters (word list inhooks/simplified.txt, the single source of truth for this repo; a missing list skips that check in silence), mojibake (U+FFFD and double-encoding remnants), and non-UTF-8 encoding (decided byiconv; noiconvskips that check). It skips binaries, generated files, and the three files whose subject is those very characters (simplified.txt,ste100-guard.sh,lang-guard.sh). Turn it off withJSC_LANG_GUARD=off. The rule text lives only injsc-meta'sreferences/ste100.md.jsc-wrap.shruns both sweeps after the CLI exits and always returns the CLI's own exit code. Asweephit warns on stderr and changes nothing else — never let a language or comment warning turn a successful CLI run into a failed one.tools/report-error.shis operator- or skill-invoked only. Never wire it to fire from a failing hook: hooks stay silent and exit 0, and a failing hook that reports itself can loop.- Data lands in
$JSC_HOME(default~/.jsc), consumed byjsc-log:worklogandjsc-log:stats.