三份 manifest 取 develop 的版號再往上升一版:分支停在一個比 develop 舊的版號, 取它等於把版號往回退。 行為清單兩支技能的節整節取 develop 那一版,再把分支新增的收尾說明逐段原文 接回去。develop 那一邊在這條分支開出去之後改寫了根目錄解析與外部呼叫兩欄, 分支那一邊只在關鍵步驟與可驗證跡象兩欄的尾端附加收尾事件的說明——兩件事互不 相干,取任一邊都會弄丟另一邊。 解這一段時弄壞過一次:第一版拿字元級差異把新增片段逐塊疊回去,中文被切碎重組, 「決定結局的那支工具的結束碼」變成「決定結局的那工具的結束碼」,關鍵步驟那一 整列還整列消失。行為清單檢核抓到表格只剩四列才發現。改成整節從 develop 重取、 只接分支相對它自己基底的那一段尾端附加,切點在共同前綴的盡頭,不切中文。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34 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.
Path rule
Every script call in this skill is written as a literal absolute path. A path that still carries $JSC_HOME, any other unexpanded variable, or a ~ cannot be resolved statically by the permission layer, so it is treated as unknown and always asks for approval. An unattended round has nobody to approve, so it stops at the first script and the whole install never starts.
Measured on this machine: $JSC_HOME/current/jsc-assist/tools/patrol.sh and ~/.jsc/current/... were both blocked and the command never ran; the same script at /root/.jsc/current/... ran. Adding an allow rule that itself starts with $JSC_HOME changed nothing on a retest, because the rule is matched against the expanded command — widening the permission list is not the fix.
Do not trade this back for portability. A variable-form path in this file buys no portability; it buys a round that dies before its first stage. Portability lives in the prerequisite below, which resolves the roots once, on the machine, at run time.
Prerequisite — resolve the roots once
Run these two before any other call in this skill, and only here:
readlink -f "$JSC_HOME/current"prints the link farm as a literal absolute path. Call it{JSC_ROOT}. Every cross-domain call is written{JSC_ROOT}/jsc-{domain}/...with that path substituted in.readlink -f "$JSC_HOME/current/jsc-hooks"prints the physical root behind thejsc-hookslink. Call it{HOOKS_ROOT}.tools/wire-cli.shis called from there and from nowhere else. That script rewrites the very{JSC_ROOT}/jsc-hookslink it would be running through and derives its own root from$0, so running it through the link makesln -sfnpoint that link at itself. The loop takes every CLI's hooks down at once, and it has happened.
Both results are checked before anything else runs: [ -d "{JSC_ROOT}" ] and [ -d "{HOOKS_ROOT}" ], each in the same approved step as its own readlink. A readlink that printed something is not a readlink that found something. With JSC_HOME unset the first call prints /current and exits 0 — non-empty, absolute, and wrong — and every literal path built from it then names a place that is not there; the second call has the same hole one level down. An empty result, a non-zero exit, a path that is not absolute, or a directory that does not exist stops the skill here: report which of the two roots did not resolve and what the command printed, say jsc-cli:deploy has to run to restore current and its jsc-hooks link, and wire nothing. Never guess a root, never fall back to a versioned plugin cache path, and never create either root here — a run that pushes on wires every CLI to scripts that are not there, and purge has already removed the hooks that worked.
Resolve both once, here. Do not re-resolve per call, and do not add a tool that prints these paths — two readlink runs and their two checks are the whole step. The main agent resolves them and hands both literal paths to every sub agent it starts, so a sub agent never resolves anything itself. Done when you hold two literal absolute paths, both naming directories that exist, and every later call starts with one of them.
Wiring
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_ROOT}/jsc-hooks, not the versioned plugin cache path and not the development checkout. wire-cli.sh expands that root itself, so what lands in each CLI's config is already a literal absolute path. {HOOKS_ROOT}/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 with one deliberate exception: use ${CLAUDE_PLUGIN_ROOT} only where the host provides it, and fall back to the manifest's own text, ${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks, for any other CLI reading the same manifest, so an unset Claude-only variable never expands into /hooks/.... That one stays in variable form on purpose: it is file content shipped with the repo, expanded by the hook's own shell on whatever machine reads it, and it never passes through the permission layer. It is not a path anyone types at a prompt, so the path rule above does not reach it.
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. Run{JSC_ROOT}/jsc-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
{HOOKS_ROOT}/tools/wire-cli.sh {cli}is what refreshes the shared{JSC_ROOT}/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.{HOOKS_ROOT}/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.{HOOKS_ROOT}/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.{HOOKS_ROOT}/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.{HOOKS_ROOT}/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.{JSC_ROOT}/jsc-hooks/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— run{JSC_ROOT}/jsc-hooks/tools/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. The error page and the error directory page live in two different wiki repos, resolved separately: the page throughwiki-repo ERROR, the directory throughwiki-repo CONTENTS. Exit 0 with anERROR_{HASH}page name and URL on stdout means the page was written; the same exit 0 with a[jsc]line on stderr still means the page landed, and that line says what is missing — the directory repo would not resolve, so nothing indexes the page; the page URL could not be read back, so the page name comes out on its own; or the URL failed the reachability check, so the directory row carries the page name as plain text with no link — carry that note into step 4. Every link on that row is written as[{text}]({url})with the URL fromgitea.sh wiki-url, and the script checks it withjsc-gitea/tools/link-check.shbefore writing: exit 0 writes the link, anything else keeps the row and drops the link, and none of it changes the exit code — this is the failure-reporting path, so a failed report must never become a second failure. Exit 0 with no output at all means the run ended on one of the quiet-degradation reasons listed in the script's own header — nogitea.shon the path, the error page's wiki repo unresolved, the hash not computed, or a temp file not created — so no page was written at all and that reason goes 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 with its URL, a page name plus the reason the URL is missing, 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. -
Record how the whole install ended. Run
tools/report-status.sh skill-end jsc-hooks:hooks-install {status} {exit} "{detail}"— the script is in this same repo, so it takes the plaintools/path that every other stage above uses. The main agent makes this one call, after every per-CLI report is in. The per-CLI pipelines run as parallel sub agents and one skill run is one event, so a call inside those sub agents would write one line per CLI and turn the install's outcome into five contradictory ones. The gate that records a skill's start fires when the skill is loaded and can never see how it ended; without this line a finished install and an install abandoned halfway look identical afterwards, which is the whole reason the closing step exists.{status}is one of five.ok: every detected CLI passed all five stages and every one of them reportedwired— in practice that means kiro was not on the machine.degraded: the pipeline ran to the end and part of it did not reachwired. That covers kiro, which isdegradedby design because the CLI cannot block a skill call, and it covers a CLI whose stage failed and was handed tojsc-hooks:repairwith a PR againstdevelop— the failure has an owner and a fix in flight, so the install is incomplete, not broken. AskippedCLI belongs here too.failed: a stage failed and the failure was left with nobody holding it —jsc-hooks:repaircould not be started, or it came back with no PR — so a broken hook stays wired and nothing is going to fix it.blocked:wire-cli.shrefused with exit 6 underJSC_READONLY=1, so no CLI was purged or wired at all.aborted: step 1 detected no CLI, so there was nothing to wire and the run stopped on a precondition rather than on an error.- Take
{exit}from the stage that decided the ending — thewire-cli.sh,smokeorscan-hook-errors.shcode — and otherwise use 0 forokand 1 for every other status.{detail}is optional, one line, at most 200 characters: the CLI count per verdict, or the CLI and stage that failed. Never fold the five per-CLI reports into it; those go to the user in step 4. - Reporting never changes the install.
report-status.shswallows its own write failures and always exits 0, and an absent file is skipped in silence — the same rule the nine hooks follow, and for the same reason: a reporter that can fail the thing it reports on is worse than no reporter. Done when the one call was made, or the script was absent and this step was skipped without a word.
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.