원클릭으로
update-zskills
Install or update Z Skills supporting infrastructure (CLAUDE.md rules, hooks, scripts)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Install or update Z Skills supporting infrastructure (CLAUDE.md rules, hooks, scripts)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Step-by-step guide for adding new block types. Use when the user asks to "add a block", "create a new block", or "implement a block".
Create an example model for one or more block types. Handles model file construction, registration, unit + codegen tests, screenshots, and verification.
Design guidelines for creating well-laid-out block diagrams and state charts. Use when building or reviewing model files.
Generate a project briefing: worktree status, open checkboxes, recent commits. Modes: summary (default), report, verify, current, worktrees. Period: 1h, 6h, 24h, 2d, 7d.
Post-PR-merge local normalization: fetch-and-prune origin, switch off merged feature branches, pull main, and delete local branches whose upstream is gone, whose PR has merged, or whose tip is fully contained in main (0 commits ahead). Preview by default — run `/cleanup-merged apply` to execute. `local` (default) / `remote` / `all` pick the scope; pass explicit branch names to narrow the candidate set. `--force` overrides the merged-check + unpushed guard for branches you explicitly name. Protected branches from config are NEVER deleted (even with `--force`) — they are always skipped.
Safe commit workflow with optional scope hint. Inventories all changes, classifies related vs. unrelated files, traces dependencies, protects other agents' work, and optionally pushes, lands worktree commits, or opens a PR via /land-pr. Positional auto enables auto-merge (PR mode only).
| name | update-zskills |
| argument-hint | [install] [locked-main-pr|direct|cherry-pick] |
| description | Install or update Z Skills supporting infrastructure (CLAUDE.md rules, hooks, scripts) |
| metadata | {"version":"2026.06.06+f0f43f"} |
Install or update the supporting infrastructure that Z Skills depend on: CLAUDE.md agent rules, safety hooks, helper scripts, and skill dependencies.
Invocation:
/update-zskills [install | --rerender | --migrate-paths | --switch-install-path={to-plugin|to-update-zskills}]
[cherry-pick | locked-main-pr | direct]
[--with-addons | --with-block-diagram-addons]
Default mode (no argument): smart detection — if nothing is installed yet, do a full install; if already installed, pull latest, update changed skills, and fill new gaps. Always begins with an audit and reports what was found and what was done about it.
Explicit mode:
install — force a full first-time setup (same as what the default
mode does when nothing is installed, but skips the detection step)--rerender — regenerate .claude/rules/zskills/managed.md against
the current .claude/zskills-config.json. Simple full-file rewrite
of the zskills-owned rules file; root ./CLAUDE.md is never touched.
No audit, no preset, no hooks/scripts touched. See
### Step D — --rerender for the algorithm.--migrate-paths — one-shot deterministic relocation of legacy
artifacts into the path-config layout (docs/plans/ for plan files,
.zskills/audit/ for forensic + narrative reports, docs/issues/
for issue trackers, .zskills/dev-server.{pid,log} for runtime
state). Dispatches to
bash $ZSK/scripts/migrate-paths.sh "$MAIN_ROOT" (where $ZSK is
.claude/skills/update-zskills shipped, or skills/update-zskills
in zskills source tree). Writes a .pre-paths-migration manifest
(write-once), updates .gitignore, and writes output.plans_dir
output.issues_dir + output.reports_dir LAST (atomic
both-or-all-or-neither — 3-tuple). The script
triggers --rerender AS THE FIRST FILE-SYSTEM CHANGE so the
broadened recursive-delete hook regex protects the migration's own
filesystem actions. Idempotent — refuses to re-run if
.pre-paths-migration already exists. The agent-runnable
follow-up (path-config-upgrade prompt) handles start-dev.sh /
stop-dev.sh rewrites and any cross-references in plan content.--switch-install-path={to-plugin|to-update-zskills} — the supported
entry point for switching a consumer between the two install lanes
(the plugin lane and the legacy /update-zskills lane). This sub-mode
is a thin delegation: it runs
bash scripts/switch-install-path.sh --to-plugin (or
--to-update-zskills) and reports its output. (The lane-switch script
is a repo-root scripts/ tool, not a skill-owned $ZSK/scripts/ one —
it operates on the consumer's .claude/ and is shared across both
lanes.) The script is
bidirectional and writes the lock file
.claude/zskills-install-lane LAST in BOTH directions (config/state
writes first, lock claim last — per CLAUDE.md ## Migration scripts),
so an interrupted switch leaves the consumer re-runnable.
=to-plugin — switch FROM the /update-zskills lane TO the plugin
lane: strips zskills hook entries from .claude/settings.json (via
scripts/migrate-strip-settings.py), basename-gated removal of the
mirrored .claude/skills/<zskills>/, .claude/hooks/<zskills>.sh,
and .claude/rules/zskills/managed.md (consumer-authored skills/
hooks are preserved), then writes plugin to the lock. The script
prints the /plugin marketplace add + /plugin install zs@zskills
steps the user runs in their Claude session.=to-update-zskills — switch FROM the plugin lane TO the
/update-zskills lane: sentinel-gated removal of the 5
plugin-materialised artifacts (only the ones STILL carrying a
zskills-materialised: sentinel — sentinel-less / re-installed
files are preserved), then writes update-zskills to the lock..zskills/ runtime
state (claim markers etc. are lane-independent). See
docs/plans/PLUGIN_DISTRIBUTION.md (D25) and docs/guides/switching-install-lanes.md
for the Abort/Rollback path.Preset keywords (bare word, anywhere in the args):
Presets are config-only: they set execution.landing and
execution.main_protected in .claude/zskills-config.json. The
main-push gate in block-unsafe-generic.sh is no longer set by the preset
— the hook reads execution.main_protected from config at runtime
(fail-closed). Everything else in zskills-config.json is preserved.
| Preset | execution.landing | execution.main_protected | push gate (derived at runtime) |
|---|---|---|---|
cherry-pick (default) | cherry-pick | false | allow |
locked-main-pr | pr | true | block |
direct | direct | false | allow |
Behavior by invocation:
/update-zskills <preset> (a bare preset — a preset keyword with NO
install mode token) — config-only. Overwrite ONLY the two
preset-owned fields above (execution.landing,
execution.main_protected) in .claude/zskills-config.json; every
other field (branch_prefix, tests, CI, dev_server, UI patterns,
timezone, min_model) is preserved. Do NOT audit, pull, or update
skills — a bare preset is a pure landing-mode switch, not a refresh.
No greenfield prompt. After writing, print a config-only confirmation
(see "Config-only confirmation + version nudge" below) that makes clear
nothing was pulled, plus a best-effort version-availability nudge. See
the dedicated ### Bare-preset config-only short-circuit step for
the algorithm./update-zskills install <preset> (a preset composed with the explicit
install mode token) — install AND set config: run the full install
(audit + fill all gaps) AND apply the preset via Step F. Unchanged./update-zskills and no existing .claude/zskills-config.json —
ask the user the greenfield prompt (see Step 0.6), then apply the
chosen preset and write the config (as part of the install/update pass)./update-zskills and existing config, no preset arg — respect the
existing config; do NOT re-ask. This is the idempotent re-install /
update path (smart detection — audit + pull + update). Unchanged.Add-on flags:
--with-addons — install/update core skills + ALL available add-on packs--with-block-diagram-addons — install/update core skills + block-diagram
add-on (3 skills: /add-block, /add-example, /model-design)Without an add-on flag, only the 23 core skills are installed/updated. If core is already installed, adding an add-on flag just copies the add-on skills (the audit detects core is satisfied and skips it).
This step runs before any mode. The portable assets (hooks, scripts,
CLAUDE_TEMPLATE.md, skills) can come from two sources: the zskills-portable/
vendored directory (inside projects like yours), or the Z Skills repo
root (which has the same structure). To find them:
Check if zskills-portable/ exists in the current working directory. If
yes, use it as $PORTABLE.
Check if zskills/ exists in the current directory and contains
CLAUDE_TEMPLATE.md. If yes, it's a repo clone — use zskills/ as
both $PORTABLE and $ZSKILLS_PATH.
Check if /tmp/zskills exists and contains CLAUDE_TEMPLATE.md. If
yes, use it.
Extended probe — common downstream clone locations. If none of
the above matched, check the following paths IN ORDER (first valid
wins). A path is valid iff the directory exists and contains all
four of CLAUDE_TEMPLATE.md, hooks/, scripts/, and skills/ —
the same validity test as the existing tiers. If the path is a git
clone, also store it as $ZSKILLS_PATH.
$PWD/../zskills (project's sibling)$PWD/../../zskills (grandparent-sibling)~/src/zskills~/code/zskills~/projects/zskills~/zskillsTrack each location you checked (matched and unmatched) for the stop-and-ask prompt below.
Stop-and-ask fallback. If no path above matched, do NOT silently
auto-clone. Instead, print the full list of locations that were
checked (tiers 1-3 plus the six extended-probe paths from tier 4),
then ask the user in plain conversation text (NOT
AskUserQuestion, per Key Rule 7):
Couldn't locate zskills source. Checked:
- ./zskills-portable/
- ./zskills/
- /tmp/zskills
- $PWD/../zskills
- $PWD/../../zskills
- ~/src/zskills
- ~/code/zskills
- ~/projects/zskills
- ~/zskills
Options: (a) paste a path to your clone (b) type
cloneto clone fresh to /tmp/zskills (c) typeabortto cancel
Wait for the user's reply. Then:
CLAUDE_TEMPLATE.md + hooks/ + scripts/ + skills/).
If valid, use it as $PORTABLE (and $ZSKILLS_PATH if it's a
git clone). If invalid, report what's missing and re-ask the same
options once; on a second invalid reply, treat as abort.clone: Fall through to the auto-clone behavior below.abort: Print "Aborted — no zskills source resolved." and
exit cleanly. Do not modify the project.abort.Auto-clone fallback (only when the user typed clone above).
Clone the repo:
git clone https://github.com/zeveck/zskills.git /tmp/zskills
If /tmp/zskills already exists, pull instead:
git -C /tmp/zskills pull
If the clone/pull fails (network, permissions), report the error clearly and stop — do not silently continue without portable assets. Tell the user:
Using Z Skills repo at /tmp/zskills for portable assets.
Portable asset detection: A valid portable source contains
CLAUDE_TEMPLATE.md, hooks/, scripts/, and skills/. The Z Skills
repo root has these at the top level (no zskills-portable/ subdirectory).
If the audit finds no gaps (all hooks, scripts, and CLAUDE.md rules already present — e.g., because the LLM already copied everything), the portable assets are not needed and Step 0 can return early.
Store the resolved path as $PORTABLE for use in install/update modes.
If the source is a git repo, also store it as $ZSKILLS_PATH for use
in update mode.
--migrate-paths short-circuit (Phase 5a)If the invocation arguments contain the bare flag --migrate-paths, this
takes precedence over every other mode (preset, install, --rerender). Run
the deterministic mover and exit; do not run the audit or any install/
update path.
Per-fence allow-hardcoded markers. The four fenced code blocks in this
section contain forbidden literals (plans/, reports/, SPRINT_REPORT.md,
etc.) that the conformance hook flags. The marker on the line preceding
each fence whitelists the block. Phase 5a ships 4 such markers; Phase 5b
adds 4 more in a separate section (total 8).
Dispatcher:
# $ZSK = .claude/skills/update-zskills (shipped) or skills/update-zskills
# (zskills source tree).
ZSK=".claude/skills/update-zskills"
[ -d "skills/update-zskills" ] && ZSK="skills/update-zskills"
MAIN_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
bash "$ZSK/scripts/migrate-paths.sh" "$MAIN_ROOT"
What the script moves (summary):
Forensic + narrative reports (Tier 2 — regenerable):
SPRINT_REPORT.md, FIX_REPORT.md, PLAN_REPORT.md,
VERIFICATION_REPORT.md, NEW_BLOCKS_REPORT.md
reports/**
plans/PLAN_INDEX.md
→ .zskills/audit/
Plans (Tier 1 — durable):
plans/*_PLAN.md, plans/CANARY*.md, plans/blocks/**
→ docs/plans/ (or $output.plans_dir if user-set)
Issue trackers:
plans/{ISSUES_PLAN,BUILD_ISSUES,DOC_ISSUES,QE_ISSUES}.md
→ docs/issues/ (or $output.issues_dir if user-set)
Work-trail reports (Tier 1.5 — durable narrative artifacts):
/run-plan plan-{slug}.md, /verify-changes verify-{name}.md,
/fix-issues SPRINT_REPORT.md
→ docs/reports/ (or $output.reports_dir if user-set;
legacy fallback `.zskills/audit`)
Runtime files:
legacy var/dev.{pid,log}
→ .zskills/dev-server.pid, .zskills/dev-server.log
Algorithm ordering (Phase 5a Locked Decisions):
The script executes 11 steps in a deterministic order. Hook-rerender is
HOISTED to step 2.5 (BEFORE any file moves) so the broadened recursive-
delete hook regex protects the migration's own filesystem actions. The
config-key write (step 10) is LAST so a mid-failure leaves the consumer
recovering via the helper's legacy-plans/ fallback.
1. Detection — refuse re-run if .pre-paths-migration already exists.
2. Resolve target dirs in memory only (no config write yet).
2.5 Trigger --rerender BEFORE any file moves (hook strengthens FIRST).
3. Move forensic + narrative reports → .zskills/audit/.
4. Move plans → $TARGET_PLANS (default docs/plans/).
4b. Move plans/PLAN_INDEX.md → .zskills/audit/.
5. Move issue trackers → $TARGET_ISSUES (default docs/issues/).
6. Move var/ runtime files → .zskills/dev-server.{pid,log}.
7. Update .gitignore (idempotent) + verify via git check-ignore -v.
8. (reserved — was --rerender step before round-2 plan hoisted to 2.5).
9. Write .pre-paths-migration manifest (write-once).
10. Write config keys (BOTH or NEITHER — atomic) LAST.
11. Print summary.
Stub-script handling (DEFER). tier1-shipped-hashes.txt does NOT
cover start-dev.sh / stop-dev.sh. The migration script does NOT
attempt auto-edit of these scripts; it prints a deferral notice naming
both files when the legacy var/dev.{pid,log} are moved. The agent-
runnable upgrade prompt (Phase 5b, references/path-config-upgrade.md)
handles them.
Recovery. If the mover aborts mid-way (a git mv fails, a git check-ignore -v returns negative, etc.) it exits non-zero and leaves
the partial state. Because the config write is LAST, the helper falls
back to legacy plans/ for any un-moved files — partial-but-functional
state, not broken. The user can re-run after fixing the underlying
cause; the idempotent guard (manifest existence) prevents double-moves.
Idempotent re-run. If .pre-paths-migration already exists, the
script prints "already migrated" and exits 0 without making any
changes. To force a fresh migration after a prior aborted run, the
user removes .pre-paths-migration AND restores files from the
manifest's from-column paths.
Example output:
moved: SPRINT_REPORT.md → .zskills/audit/SPRINT_REPORT.md
moved: plans/FOO_PLAN.md → docs/plans/FOO_PLAN.md
...
Wrote .pre-paths-migration with N entries.
Re-rendered hooks (broadened recursive-delete fence — applied EARLY).
Wrote output.plans_dir = "docs/plans", output.issues_dir = "docs/issues", and output.reports_dir = "docs/reports".
For start-dev.sh / stop-dev.sh customizations, see
.claude/skills/update-zskills/references/path-config-upgrade.md.
After dispatch, /update-zskills --migrate-paths exits with the script's
exit code. Do NOT proceed to Step 0.25 / 0.5 / audit.
migrate-paths.sh runs a structural-reference rewriter immediately after
gitignore update (step 7) and before the manifest write (step 9). The
rewriter scans every .md under <TARGET_PLANS> and rewrites legacy
plans/X.md and reports/Y.md tokens to the migrated paths, gated by
the YAML frontmatter status: field.
Frontmatter decision tree:
status: active → REWRITE (all 4 enclosure types)
status: proposal → REWRITE
(no frontmatter) → REWRITE
status: complete + filename CANARY*.md → REWRITE (slash-command lines
naturally limited by rule 4)
status: complete + non-canary filename → PRESERVE (frozen) + scan/warn
status: deferred / paused / other → PRESERVE + scan/warn
Four enclosure types triggering rewrite — token must appear inside ONE:
1. Markdown link: [...](plans/X.md) or [...](reports/Y.md)
2. Backtick span: `plans/X.md` or `reports/Y.md`
3. Shell line: inside ```bash/```sh/```shell/``` fence,
OR line starts with "$ ",
OR line ends with shell metachar | > < ;
4. Slash-command: /run-plan plans/X.md (also draft-plan, refine-plan,
draft-tests, work-on-plans, research-and-plan,
research-and-go)
Substitution targets:
plans/X.md → <TARGET_PLANS>/X.md (e.g., docs/plans/X.md)
reports/<slug>-Y.md → .zskills/audit/<slug>-Y.md
(slug ∈ {plan, verify, briefing, new-blocks})
Warning emission contract. For PRESERVED plans containing legacy
tokens, the rewriter emits a stderr WARN line per hit AND appends the
same line to .pre-paths-migration-warnings at the repo root:
WARN docs/plans/OLD_FEATURE.md:42: legacy token 'plans/OTHER.md' preserved (frozen plan; see path-config-upgrade.md)
--rewrite-only flag. For mid-version-skip recovery (when an older
5a-only migrate-paths.sh ran without cross-ref rewrite), the agent-
runnable upgrade prompt at references/path-config-upgrade.md invokes
migrate-paths.sh --rewrite-only "$MAIN_ROOT". This skips steps 1–7,
resolves <TARGET_PLANS> from the existing config, runs ONLY the cross-
ref rewrite, and appends a rewrite-only: <ts> <count> trailer to the
existing manifest. Config keys are not re-written. Idempotent.
Scan the invocation arguments for one of these bare keywords (order
doesn't matter; no preset= prefix; must be a whole word):
cherry-picklocked-main-prdirectRecord the match as $PRESET_ARG. If none is present, $PRESET_ARG is
empty. If more than one is present, stop with an error: "Specify exactly
one preset: cherry-pick, locked-main-pr, or direct."
Parser pseudocode (classify each token; presets, mode, and add-on flags are orthogonal and can coexist):
PRESET_ARG=""
MODE="" # "install" or "" (default = smart detection)
ADDON_FLAG="" # --with-addons | --with-block-diagram-addons | ""
for tok in $ARGUMENTS; do
case "$tok" in
cherry-pick|locked-main-pr|direct)
[ -n "$PRESET_ARG" ] && fail "multiple presets"
PRESET_ARG="$tok" ;;
install) MODE="install" ;;
--with-addons|--with-block-diagram-addons) ADDON_FLAG="$tok" ;;
*) ;; # unknown token — ignore, don't error
esac
done
install + a preset keyword are compatible and combine (force-install
with the chosen preset). --with-addons / --with-block-diagram-addons
are independent of the preset — they control only which skills get
installed, not landing behavior.
Preset → field mapping (used wherever a preset is applied in later steps):
$PRESET_ARG | execution.landing | execution.main_protected |
|---|---|---|
cherry-pick | "cherry-pick" | false |
locked-main-pr | "pr" | true |
direct | "direct" | false |
These two config fields are preset-owned. (The main-push gate in
block-unsafe-generic.sh derives from main_protected at runtime — it is
not a value the preset writes.) When $PRESET_ARG is
non-empty, every other field in .claude/zskills-config.json
(branch_prefix, testing.*, dev_server.*, ui.*, ci.*,
timezone, agents.min_model) is preserved unchanged.
Check if .claude/zskills-config.json exists in the target project root ($PROJECT_ROOT).
If it exists:
Read the file content.
Extract values using bash regex (pure bash, no external JSON tool).
IMPORTANT — parent-object scoping is mandatory for any field
that lives inside a parent block. An unscoped regex like
\"unit_cmd\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" matches the
FIRST occurrence of the field anywhere in the JSON, so a future
sibling block declaring its own same-named field will silently
shadow the intended one. The fix class addressed by issues #395
(zskills-resolve-config.sh) and #400 (apply-preset.sh) and
reproductively-broad issue #428 (this prose recipe) is the SAME
bug class: every field that lives inside "<parent>": { ... }
must scope its extraction under the parent. The canonical form
for a scoped string-value extraction is:
\"<parent>\"[[:space:]]*:[[:space:]]*\{[^}]*\"<field>\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"
Top-level fields (project_name, timezone) have no parent
block and may be extracted directly. The mapping below annotates
every parent-scoped field with # parent: <name> next to its
regex.
CONFIG_CONTENT=$(cat "$PROJECT_ROOT/.claude/zskills-config.json")
# Top-level string (no parent — direct extraction is safe):
if [[ "$CONFIG_CONTENT" =~ \"project_name\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
PROJECT_NAME="${BASH_REMATCH[1]}"
fi
# parent: testing (sibling-shadow risk if unscoped — see #395)
if [[ "$CONFIG_CONTENT" =~ \"testing\"[[:space:]]*:[[:space:]]*\{[^}]*\"unit_cmd\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
UNIT_CMD="${BASH_REMATCH[1]}"
fi
# parent: testing
if [[ "$CONFIG_CONTENT" =~ \"testing\"[[:space:]]*:[[:space:]]*\{[^}]*\"full_cmd\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
FULL_CMD="${BASH_REMATCH[1]}"
fi
# parent: testing
if [[ "$CONFIG_CONTENT" =~ \"testing\"[[:space:]]*:[[:space:]]*\{[^}]*\"output_file\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
OUTPUT_FILE="${BASH_REMATCH[1]}"
fi
# parent: dev_server
if [[ "$CONFIG_CONTENT" =~ \"dev_server\"[[:space:]]*:[[:space:]]*\{[^}]*\"cmd\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
DEV_SERVER_CMD="${BASH_REMATCH[1]}"
fi
# parent: dev_server
if [[ "$CONFIG_CONTENT" =~ \"dev_server\"[[:space:]]*:[[:space:]]*\{[^}]*\"main_repo_path\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
MAIN_REPO_PATH="${BASH_REMATCH[1]}"
fi
# parent: ui (note: `file_patterns` also exists under `testing` as
# an array — scoping under `ui` disambiguates)
if [[ "$CONFIG_CONTENT" =~ \"ui\"[[:space:]]*:[[:space:]]*\{[^}]*\"file_patterns\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
UI_FILE_PATTERNS="${BASH_REMATCH[1]}"
fi
# parent: ui
if [[ "$CONFIG_CONTENT" =~ \"ui\"[[:space:]]*:[[:space:]]*\{[^}]*\"auth_bypass\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
AUTH_BYPASS="${BASH_REMATCH[1]}"
fi
# Top-level string (no parent):
if [[ "$CONFIG_CONTENT" =~ \"timezone\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
TIMEZONE="${BASH_REMATCH[1]}"
fi
# parent: execution (boolean — sibling-shadow risk if unscoped, see #400)
if [[ "$CONFIG_CONTENT" =~ \"execution\"[[:space:]]*:[[:space:]]*\{[^}]*\"main_protected\"[[:space:]]*:[[:space:]]*(true|false) ]]; then
MAIN_PROTECTED="${BASH_REMATCH[1]}"
fi
# parent: execution (landing mode)
if [[ "$CONFIG_CONTENT" =~ \"execution\"[[:space:]]*:[[:space:]]*\{[^}]*\"landing\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
LANDING_MODE="${BASH_REMATCH[1]}"
fi
# parent: execution (branch prefix)
if [[ "$CONFIG_CONTENT" =~ \"execution\"[[:space:]]*:[[:space:]]*\{[^}]*\"branch_prefix\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
BRANCH_PREFIX="${BASH_REMATCH[1]}"
fi
# parent: ci (boolean)
if [[ "$CONFIG_CONTENT" =~ \"ci\"[[:space:]]*:[[:space:]]*\{[^}]*\"auto_fix\"[[:space:]]*:[[:space:]]*(true|false) ]]; then
CI_AUTO_FIX="${BASH_REMATCH[1]}"
fi
# parent: ci (integer)
if [[ "$CONFIG_CONTENT" =~ \"ci\"[[:space:]]*:[[:space:]]*\{[^}]*\"max_fix_attempts\"[[:space:]]*:[[:space:]]*([0-9]+) ]]; then
CI_MAX_ATTEMPTS="${BASH_REMATCH[1]}"
fi
# parent: commit (optional — backfilled below if missing)
if [[ "$CONFIG_CONTENT" =~ \"commit\"[[:space:]]*:[[:space:]]*\{[^}]*\"co_author\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
CO_AUTHOR="${BASH_REMATCH[1]}"
fi
The [^}]* between the parent key and the field key is what
constrains the match to inside that block. Do NOT replace it with
.* (which would match across blocks) and do NOT drop the parent
prefix to "simplify" the regex — that re-introduces the bug class.
For each template placeholder, use the config value if non-empty.
3.5. Backfill commit.co_author if absent. If the existing config
does not contain a "commit" block with a "co_author" field (e.g.
configs written before this field was introduced), splice in the
default so downstream skills (/commit) can rely on the
field resolving. Default value:
"Claude Opus 4.7 (1M context) <noreply@anthropic.com>". Match the
same style used for other optional-field backfills — a targeted
Edit or small sed-based rewrite that preserves every other field unchanged.
If the commit key is absent, add the whole block; if the commit
block exists but lacks co_author, add only that field. Idempotent:
re-running on an already-backfilled config is a no-op.
> **Path-config keys: FRESH-SCAFFOLD writes them; auto-backfill does NOT.**
> A FRESH config scaffold (both lanes — this install's `Write` below and
> the plugin lane's SessionStart materialiser seed) DOES write
> `output.plans_dir = "docs/plans"`, `output.issues_dir = "docs/issues"`,
> and `output.reports_dir = "docs/reports"`, so a brand-new consumer's
> dashboard and plan-skills find plans in `docs/plans` out of the box.
> But these keys MUST NEVER be auto-BACKFILLED into an EXISTING config
> that lacks them during install or `--rerender`. Their absence in an
> existing config is meaningful — the helper falls back to legacy
> `plans/`, preserving consumer-current behavior. Relocating an EXISTING
> legacy layout is owned by `/update-zskills --migrate-paths`, which
> writes these keys (and writes BOTH or NEITHER).
> See plan `docs/plans/ZSKILLS_PATH_CONFIG.md` (or
> `plans/ZSKILLS_PATH_CONFIG.md` pre-migration).
Copy config/zskills-config.schema.json from $PORTABLE to
.claude/zskills-config.schema.json in the target project (so the
$schema reference in the config resolves correctly).
If $PRESET_ARG was set, defer preset application to
Step F — Apply Preset (invoked at the end of both install and
update paths). Step F runs .claude/skills/update-zskills/scripts/apply-preset.sh which is
config-only — it sets the two preset-owned config fields
(execution.landing, execution.main_protected) atomically,
including idempotency, JSON formatting variance, and a missing
execution key. It does NOT touch the hook; block-unsafe-generic.sh
reads main_protected from config at runtime. Don't attempt a manual
Edit here — the script is the single source of truth.
If it does not exist:
If $PRESET_ARG is empty, run the greenfield prompt (Step 0.6)
to pick a preset. Otherwise skip the prompt and use $PRESET_ARG.
Auto-detect values from the project (existing behavior).
Write the config file directly using the Write tool. Running
/update-zskills is the user's consent — do not gate this on a paste-this-
heredoc step. If the user's permission mode prompts for the write, that is
Claude Code's normal flow and the user will approve.
Content to write to .claude/zskills-config.json:
{
"$schema": "./zskills-config.schema.json",
"project_name": "<detected>",
"timezone": "America/New_York",
"execution": {
"landing": "<preset.landing>",
"main_protected": <preset.main_protected>,
"branch_prefix": "feat/"
},
"commit": {
"co_author": "Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
},
"testing": {
"unit_cmd": "<detected>",
"full_cmd": "<detected>",
"output_file": ".test-results.txt",
"file_patterns": ["<detected>"]
},
"dev_server": {
"cmd": "<detected>",
"default_port": 8080,
"main_repo_path": "<detected>"
},
"ui": {
"file_patterns": "",
"auth_bypass": ""
},
"ci": {
"auto_fix": true,
"max_fix_attempts": 2
},
"agents": {
"min_model": "auto"
},
"output": {
"plans_dir": "docs/plans",
"issues_dir": "docs/issues",
"reports_dir": "docs/reports"
}
}
Substitute the three preset-owned placeholders (<preset.landing>,
<preset.main_protected>) using the Step 0.25 mapping table. Fields
left empty by auto-detection stay as empty strings — the install
summary's test-setup blurb tells the user what to fill in later.
No hook edit needed. The config's execution.landing
and execution.main_protected placeholders above are substituted
in at write time, and block-unsafe-generic.sh reads
main_protected from config at runtime to decide whether to block
a push to main/master (fail-closed). Step F — Apply Preset is
config-only and does not touch the hook. Nothing to do here.
Merge algorithm pseudocode:
for each field F in schema:
if config[F] is non-empty string (or true/false for booleans):
use config[F]
else if auto_detect[F] is non-empty:
use auto_detect[F]
else:
mark as empty -> template section gets commented out
Template placeholder mapping:
| Placeholder | Config path | Example |
|---|---|---|
{{DEV_SERVER_CMD}} | dev_server.cmd | npm start |
{{AUTH_BYPASS}} | ui.auth_bypass | localStorage.setItem(...) |
{{DEFAULT_PORT}} | dev_server.default_port | 8080 |
{{MAIN_REPO_PATH}} | dev_server.main_repo_path | /path/to/repo |
Runtime-read fields (read by hooks and helper scripts at every invocation, NOT install-filled): testing.unit_cmd, testing.full_cmd, ui.file_patterns. The field dev_server.main_repo_path is read at runtime by port.sh AND install-substituted into managed.md as {{MAIN_REPO_PATH}} (the rendered value reflects the config at install/--rerender time; warn-config-drift signals re-render-needed when the config is edited via Claude Code's Edit/Write tool — see Phase 3 Design & Constraints for coverage limits). Similarly, dev_server.default_port is runtime-read by port.sh AND install-substituted as {{DEFAULT_PORT}}. See Phase 1 of plans/DRIFT_ARCH_FIX.md for the canonical bash-regex read pattern.
Empty value handling: When a config field is empty string "", the
corresponding template section is commented out with a TODO marker:
# Example: if UI_FILE_PATTERNS is empty, comment out the UI verification section
# in block-unsafe-project.sh:
#
# Before:
# UI_FILE_PATTERNS="src/components/.*\.tsx?$"
#
# After (empty):
# # TODO: Configure UI file patterns in .claude/zskills-config.json
# # UI_FILE_PATTERNS=""
Run this only when .claude/zskills-config.json does NOT exist AND
$PRESET_ARG is empty. Skip otherwise.
Do NOT use AskUserQuestion. Ask in plain conversation text, exactly as shown. Wait for the user's reply before proceeding.
Ask:
How should /run-plan land changes?
(1) cherry-pick — each phase squash-lands directly to main (simple, solo)
(2) locked-main-pr — plans become feature branches + PRs, CI, auto-merge
(locked main, shared repo)
(3) direct — work on main, no worktree isolation (minimal, risky)
Default: (1). Pick one, or accept the default.
Map the reply:
1, cherry-pick, or an empty/default-accepting reply → cherry-pick2, locked-main-pr, or pr → locked-main-pr3, direct → direct/update-zskills locked-main-pr later to switch." —
then proceed. Never re-ask the prompt; never invent a 4th option.Set $PRESET_ARG to the chosen preset and proceed. No follow-up
questions — the two-field config mapping (landing + main_protected) in
Step 0.25 is final. In particular, we do not ask "do you want the
main-push block on?" for locked-main-pr: main_protected=true is the
single source of truth — block-unsafe-project.sh blocks agent commits,
cherry-picks, and pushes on main, and block-unsafe-generic.sh derives
its push gate from the same main_protected value at runtime, so the
push block is not a separate user-facing choice.
The audit scans the project for all Z Skills dependencies and reports what is present and what is missing. The audit itself never modifies any files. Its output is always displayed so the user can see exactly what was found before any changes are made.
List all .claude/skills/*/SKILL.md files. For each skill:
requires: field (list of skill
names), check that each required skill is also installed. Collect all
missing dependencies.npm test, npm run test:all,
{{FULL_TEST_CMD}}) — check if test commands are configured.playwright-cli, gh) — check if the tool is
available via which.python3) — check via which. Python 3 is
required (per CLAUDE.md "Python is required"). Powers /briefing,
/plans rebuild, the dashboard, and other Python-only helpers.block-unsafe) — check if the hook file
exists in .claude/hooks/..claude/skills/update-zskills/scripts/port.sh, scripts/test-all.sh) — check if
the script file exists.Read .claude/rules/zskills/managed.md (the zskills-owned rules
file); if absent, fall back to reading root ./CLAUDE.md (pre-Phase-4
installs rendered rules there). For each of the 13 generic rules,
search for a distinctive key phrase that identifies the rule
(case-insensitive). Mark the rule as present if the key phrase is
found, missing otherwise.
| # | Rule Name | Key Phrase(s) to Search |
|---|---|---|
| 1 | Never weaken tests | "loosen tolerances" or "widen thresholds" |
| 2 | Capture test output | "capture" AND "output" AND "never pipe" |
| 3 | Max 2 fix attempts | "two attempts.*maximum" or "NEVER thrash" |
| 4 | Pre-existing failures | "pre-existing" AND "it.skip" |
| 5 | Never discard others' changes | "discard" AND "changes" AND "didn't make" |
| 6 | Protect untracked files | "protect untracked" or "git stash -u" |
| 7 | Feature-complete commits | "feature-complete" AND "trace" AND "imports" |
| 8 | Landed marker check | ".landed" AND "status: full" |
| 9 | Worktree verify before remove | "worktree" AND "batch-remove" |
| 10 | Never defer hard parts | "defer" AND "hard parts" AND "future phases" |
| 11 | Correctness over speed | "correctness over speed" or "correctness, not speed" |
| 12 | Enumerate before guessing | "enumerate before guessing" |
| 13 | Never skip hooks | "never.*--no-verify" or "skip.*pre-commit hooks" |
Search the zskills rules file (.claude/rules/zskills/managed.md,
falling back to root ./CLAUDE.md) for these documentation-presence
signals. Mark each present/missing based on case-insensitive
substring match:
| Check | Key phrase(s) to search in zskills rules file |
|---|---|
| Execution Modes section | ## Execution Modes (heading) |
| Landing mode keywords documented | cherry-pick AND pr AND direct |
| Direct mode description present | Work directly on main |
Report in the same pass/fail format as Step 2. Missing items are recommendations, not errors — this is a documentation-only gap with no enforcement consequence.
Look in .claude/hooks/ for these 2 files:
block-unsafe-generic.sh (or block-unsafe.sh — either name counts)block-unsafe-project.shTier-1 machinery moved out of root scripts/ and into the owning skill's
mirror at .claude/skills/<owner>/scripts/<name> (see
references/script-ownership.md for the authoritative tier → owner table).
Check each Tier-1 script at its owner's path — NOT at root scripts/:
.claude/skills/update-zskills/scripts/port.sh.claude/skills/briefing/scripts/briefing.py — also checked in Step 5; the /briefing skill requires it.claude/skills/update-zskills/scripts/clear-tracking.sh.claude/skills/commit/scripts/land-phase.sh — referenced by /run-plan, /fix-issues, /do for atomic post-landing cleanup.claude/skills/run-plan/scripts/post-run-invariants.sh — referenced by /run-plan as mandatory end-of-run gate (7 invariants).claude/skills/commit/scripts/write-landed.sh — referenced by /run-plan, /fix-issues, /commit for rc-checked atomic .landed marker writes.claude/skills/create-worktree/scripts/worktree-add-safe.sh — referenced by /run-plan, /fix-issues, /do for safe worktree creation (discriminates fresh vs poisoned stale branches).claude/skills/create-worktree/scripts/create-worktree.sh — referenced by /run-plan, /fix-issues, /do for unified worktree creation.claude/skills/create-worktree/scripts/sanitize-pipeline-id.sh — shared PIPELINE_ID sanitizer (used by /run-plan, /fix-issues, /do before persisting ID).claude/skills/update-zskills/scripts/apply-preset.sh — required by the preset UX (Step F); config-only — updates execution.landing/execution.main_protected in config (the hook reads main_protected at runtime; apply-preset does not touch it).claude/skills/run-plan/scripts/compute-cron-fire.sh — required by /run-plan (Phase 5c chunked finish-auto, verify-pending retry, re-entry) for computing one-shot cron expressions with correct minute/hour/day/month/year rollover.claude/skills/update-zskills/scripts/statusline.sh — session statusline helper (optional but should be installed if the user has it)Tier-2 release/consumer-tooling stays at root scripts/ (per
references/script-ownership.md) — check these there:
scripts/test-all.shscripts/stop-dev.sh — sanctioned SIGTERM-only dev-server stopper (reads .zskills/dev-server.pid). The approved way for agents to stop a dev server without reaching for kill -9 / fuser -k / lsof -ti | xargs killIf /briefing is installed, check for [ -f .claude/skills/briefing/scripts/briefing.py ]
(the artifact half catches partial skill-mirror installs). If not found, add a
note: "The /briefing skill requires .claude/skills/briefing/scripts/briefing.py
— see /briefing skill documentation."
Output the report in this exact format:
Z Skills Audit Report
=====================
Skills installed: N
[list of skill names]
Skill Dependencies: all satisfied | K missing
Missing:
- /run-plan requires /verify-changes — NOT INSTALLED
...
Agent Rules: M/13 present (K missing)
Missing:
- [rule name]: [key phrase not found]
...
Execution Mode Docs: M/3 present (K missing/recommended)
Missing (recommendation only):
- [check name]: [key phrase not found]
...
Hooks: M/2 installed (K missing)
Missing:
- [filename]
...
Scripts: M/3 installed (K missing)
Missing:
- [filename]
...
Tools: M/N available (K missing)
Missing:
- [tool name]: not found in PATH
...
Skills with additional requirements:
- /briefing: requires `.claude/skills/briefing/scripts/briefing.py` (not found)
...
Overall: X/Y dependencies satisfied.
After printing Overall: ..., append a one-line Versions summary
showing the installed zskills_version (consumer-side) vs the source
clone's latest tag (authoritative), plus how many skills have a different
metadata.version upstream. Compute it like this:
if [ -f "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/scripts/zskills-resolve-config.sh" ]; then
export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
. "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/scripts/zskills-resolve-config.sh"
else
. "$CLAUDE_PROJECT_DIR/.claude/skills/update-zskills/scripts/zskills-resolve-config.sh"
fi
# Repo-level version: latest YYYY.MM.N tag in the source clone, falling
# back to the top-level `version` in .claude-plugin/plugin.json when there
# is no tag (#1124) — so a tagless-but-complete clone still resolves a
# version. Empty only when there is neither a tag nor a readable plugin.json.
current_zskills_ver=$(bash "$ZSKILLS_SKILLS_ROOT/update-zskills/scripts/resolve-repo-version.sh" "$ZSKILLS_PATH")
# Installed version: top-level `zskills_version` field in
# .claude/zskills-config.json. Read with inline BASH_REMATCH (NOT
# frontmatter-get.sh — that helper is YAML-only and exits 2 on JSON;
# silencing that with `2>/dev/null` would be the anti-CLAUDE.md
# "swallow failures" pattern. Same JSON-parsing idiom as
# zskills-resolve-config.sh — single source of truth.)
installed_zskills_ver=""
if [ -f "$CLAUDE_PROJECT_DIR/.claude/zskills-config.json" ]; then
cfg=$(cat "$CLAUDE_PROJECT_DIR/.claude/zskills-config.json")
if [[ "$cfg" =~ \"zskills_version\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
installed_zskills_ver="${BASH_REMATCH[1]}"
fi
fi
# Per-skill delta: count rows whose `metadata.version` differs upstream.
delta_tsv=$(bash "$ZSKILLS_SKILLS_ROOT/update-zskills/scripts/skill-version-delta.sh" "$ZSKILLS_PATH")
n_changed=$(printf '%s\n' "$delta_tsv" | awk -F'\t' '$5 == "bumped" || $5 == "new"' | wc -l)
Render (print the lines verbatim, substituting the three values; if
current_zskills_ver is empty, print the literal (unversioned) — source clone has no tags; if installed_zskills_ver is empty, print (none)):
Repo version: <installed_zskills_ver> → <current_zskills_ver>
Versions: zskills <installed_zskills_ver>→<current_zskills_ver>; <n_changed> skills changed
The Repo version: line uses the same label as the install/update
final reports (Step G / Pull Latest step 6) for cross-report
consistency; the Versions: line is the audit-specific one-liner that
also includes the per-skill delta count. Both surface the same
underlying data.
If the source clone has neither a tag nor a readable
.claude-plugin/plugin.json version (genuinely unversioned):
current_zskills_ver is empty and both lines still print with the
(unversioned) placeholder. This surfaces the state instead of hiding it
(CLAUDE.md surface-bugs rule). Note that a tagless-but-complete clone is
NOT unversioned — resolve-repo-version.sh falls back to the plugin.json
version (#1124), so (unversioned) now appears only when the clone is
truly missing both signals. Same (none) handling applies to a
pre-Phase-5 install with no zskills_version field — print (none); the
install/Pull-Latest path will write the field on its next run via the
mirror-the-tag step (see Step F.5 / Pull Latest step 5.7).
If everything is satisfied, end with:
Overall: Y/Y dependencies satisfied. Nothing to install.
If there are gaps and the skill is running in default or install mode, proceed to fill them (see below). The audit report is always shown first so the user sees what was found before any modifications.
This branch runs after the arg parser (Step 0.25), config read (Step 0.5),
and the greenfield prompt (Step 0.6), and at/above the Default-Mode
Smart-Detection fork below. Explicit --migrate-paths already
short-circuited at Step 0.1; --rerender and --switch-install-path are a
user forcing a config/lane action and are not intercepted by the
hard-refuse below. The destructive case this branch prevents is the silent
flip of a pure-plugin consumer on a bare /update-zskills call: with no
.claude/skills/ mirror, the audit would see every skill/hook as "missing"
and the gap-fill would copy them all in, flipping the consumer onto the
legacy lane.
Policy reversal (W6.1) — explicit install / --with-addons is now
HARD-REFUSED on the plugin lane. Earlier this branch let explicit install
through as an "opt-in" mirror action. That carve-out is removed: a client is
single-lane, so running /update-zskills install on a detect == plugin consumer is never the right move — it would re-create the legacy
mirror alongside the plugin and put the consumer into the dual state the rest
of the system actively pushes to consolidate. The refuse points the user at
scripts/switch-install-path.sh (the supported lane-switch path) and exits
non-zero. Keyed on detect_install_state == plugin, NOT
$CLAUDE_PLUGIN_ROOT — so the dev repo (which keeps its legacy mirror and
classifies update-zskills) is never blocked, and dogfooding both lanes from
this repo via /update-zskills install still works. The one carve-out: the
refuse is SKIPPED while a lane switch is in progress (see the
switch-in-progress marker below), so switch-install-path.sh --to-update-zskills — which mandates /plugin uninstall → restart →
/update-zskills install — does not deadlock against its own
not-yet-completed switch.
Signal = detect_install_state == plugin, NOT $CLAUDE_PLUGIN_ROOT.
lane == plugin means "plugin-materialised artifacts present AND no legacy
.claude/skills mirror" — the exact flip-risk. $CLAUDE_PLUGIN_ROOT is
rejected: it is set in any claude --plugin-dir . session (including the
dev repo's legacy-lane dogfooding), so keying on it would make
/update-zskills install wrongly hit this branch and refuse to install.
detect_install_state answers "what's installed on disk", not "how was I
invoked." The dev repo has its legacy mirror present, so it classifies as
update-zskills and this branch never fires there.
Resolve the lane (dual-locate + fail-soft). detect-install-state.sh
is NOT mirrored into .claude/hooks/_lib/ on the legacy lane, so locate it
under $CLAUDE_PLUGIN_ROOT or $PORTABLE; if it is unreachable, default
LANE=update-zskills (legacy behavior — safe, because the only unreachable
case is a pure-legacy session, which should proceed with legacy behavior
anyway; a pure-plugin consumer always has $CLAUDE_PLUGIN_ROOT set):
MAIN_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
DIS=""
if [ -f "${CLAUDE_PLUGIN_ROOT}/hooks/_lib/detect-install-state.sh" ]; then
export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
DIS="${CLAUDE_PLUGIN_ROOT}/hooks/_lib/detect-install-state.sh"
elif [ -n "${PORTABLE:-}" ] && [ -f "${PORTABLE}/hooks/_lib/detect-install-state.sh" ]; then
DIS="${PORTABLE}/hooks/_lib/detect-install-state.sh"
fi
LANE="update-zskills" # fail-soft default = legacy behavior (safe)
if [ -n "$DIS" ]; then . "$DIS"; LANE="$(detect_install_state "$MAIN_ROOT")"; fi
If LANE == plugin: do NOT run Step 0 asset-locate, the audit's
gap-fill (Steps A–G below), or any .claude/-mirroring step.
W6.1 hard-refuse — explicit install / --with-addons on the plugin
lane. If an explicit install arg was parsed ($MODE == "install" OR
$ADDON_FLAG non-empty) AND no lane switch is in progress, REFUSE and exit
non-zero. The switch-in-progress carve-out (W6.2) lets
scripts/switch-install-path.sh --to-update-zskills run its mandated
/update-zskills install step without tripping this refuse:
if { [ "$MODE" = install ] || [ -n "$ADDON_FLAG" ]; } \
&& [ ! -f "${CLAUDE_PROJECT_DIR:-$PWD}/.zskills/switch-in-progress" ]; then
echo "ERROR: refusing 'install' / '--with-addons' on the plugin lane." >&2
echo "A client is single-lane. Running it here would re-create the legacy" >&2
echo ".claude/skills mirror alongside the plugin (the dual state the system" >&2
echo "actively pushes to consolidate). To switch lanes, run:" >&2
echo " bash scripts/switch-install-path.sh --to-update-zskills" >&2
echo " (or /update-zskills --switch-install-path=to-update-zskills)" >&2
exit 1
fi
(The switch-install-path.sh --to-update-zskills flow writes
.zskills/switch-in-progress at its START and removes it only after the
lane-lock is written LAST, so this skip is active exactly for the duration
of an in-flight switch and not in steady state.)
If a preset arg was parsed ($PRESET_ARG non-empty): apply it
config-only via the existing Step F — Apply Preset call (it is
lane-portable — it edits only .claude/zskills-config.json, never the
mirror):
if [ -f "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/scripts/zskills-resolve-config.sh" ]; then
export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
. "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/scripts/zskills-resolve-config.sh"
else
. "$CLAUDE_PROJECT_DIR/.claude/skills/update-zskills/scripts/zskills-resolve-config.sh"
fi
bash "$ZSKILLS_SKILLS_ROOT/update-zskills/scripts/apply-preset.sh" "$PRESET_ARG"
Report the result verbatim per Step F's exit-code table (0 applied / 1
no-change / 2 usage / 3 missing config / 4 malformed config), then print
the config-only confirmation + best-effort version nudge from
### Bare-preset config-only short-circuit below, and exit with the
script's exit code. Do not proceed to the audit or any fill step.
Else (bare call, no preset): FIRST run the bundled verifier's cheap structural tier, THEN print the plugin-lane explanation and exit 0.
This is the plugin-lane analog of #### Step G.5 — Post-install verification: a read-only, NON-FATAL health check ("verify at install
completion" equivalent) for the most-common single-lane install state. A
bare /update-zskills on the plugin lane does no install work (the plugin
manager owns skills/hooks/rules), but the consumer-side install can still
be subtly broken (a registered hook resolving to nothing, an un-rendered
managed.md carrying raw {{TOKEN}} placeholders, a dropped
artifact/sentinel, an accidental dual-install). Running the verifier here
surfaces that breakage even though there is no install/update path to wire
Step G.5 into on this lane. It is read-only and NON-FATAL — it reports
PASS/WARN/FAIL but never aborts and cannot re-create the legacy
.claude/skills mirror (the verifier only inspects state). Run ONLY the
cheap structural tier here — never --deep (the heavy live-claude probe
is opt-in and must not run on a bare informational call). Resolve the
verifier path lane-awarely, exactly as Step G.5 does:
if [ -f "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/verifiers/verify-install.sh" ]; then
export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
VERIFY_INSTALL="${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/verifiers/verify-install.sh"
else
VERIFY_INSTALL="$CLAUDE_PROJECT_DIR/.claude/skills/update-zskills/verifiers/verify-install.sh"
fi
if [ -f "$VERIFY_INSTALL" ]; then
# Cheap tier only (no --deep). Non-fatal: report, never abort.
bash "$VERIFY_INSTALL" --project-dir "$CLAUDE_PROJECT_DIR" || \
echo "(post-install verification reported a FAIL above — review and fix your environment)"
else
echo "(post-install verifier not found at $VERIFY_INSTALL — skipping verification)"
fi
THEN print the plugin-lane explanation and exit 0:
You're on the plugin lane. Skills, hooks, and rules are plugin-managed —
update them with `/plugin marketplace update`, not `/update-zskills`. To
change landing mode here, run
`/zs:update-zskills <cherry-pick|locked-main-pr|direct>` or edit
`.claude/zskills-config.json` directly (config is the single source of
truth for mode). To switch install lanes, use
`scripts/switch-install-path.sh` (or
`/update-zskills --switch-install-path=...`).
Plugin-pre-materialise guard (LANE == fresh AND $CLAUDE_PLUGIN_ROOT
set) — #1080. Before the bare-preset / Default-Mode dispatch below, handle
the half-installed plugin state: a consumer who ran /plugin install zs@zskills then /reload-plugins (which does NOT fire SessionStart, so the
materialiser never ran) lands here with LANE == fresh (no on-disk evidence
of either lane yet) AND $CLAUDE_PLUGIN_ROOT set (the plugin IS loaded).
Running the fresh-install mirror here would copy in ~23 skills + the hooks +
render managed.md, dual-installing the legacy lane alongside the plugin —
the exact dual state the rest of the system pushes to consolidate. Instead,
recognize "plugin-pre-materialise", direct the user to finish setup via
/clear (or restart), and exit WITHOUT mirroring:
# #1080 — plugin loaded but not yet materialised (SessionStart didn't fire on
# /reload-plugins). Keying on $CLAUDE_PLUGIN_ROOT is SAFE *in the `fresh`
# branch specifically* because the dev repo ALWAYS carries a legacy
# `.claude/skills/` mirror -> it is NEVER classified `fresh` (it classifies
# `update-zskills` or `dual`). So this guard can never false-positive in the
# dogfooding repo, even though $CLAUDE_PLUGIN_ROOT is set there too.
if [ "$LANE" = fresh ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
echo "The zskills plugin is loaded, but setup isn't finished yet." >&2
echo "/reload-plugins does NOT run the one-time materialisation step, so the" >&2
echo "verifier/implementer agents, the timeout/validate hooks, the rendered" >&2
echo "rules file, and your .claude/zskills-config.json have not been written." >&2
echo "Run /clear (or exit and restart Claude Code) to fire the SessionStart" >&2
echo "hook and finish setup. Re-running /update-zskills is NOT the fix —" >&2
echo "it would dual-install the legacy mirror alongside the plugin." >&2
exit 0
fi
Else (LANE is update-zskills, dual, or fresh, OR detection was
unreachable): proceed to the existing behavior. (The fresh + plugin-loaded
sub-case was intercepted by the plugin-pre-materialise guard immediately
above; a fresh arrival WITHOUT $CLAUDE_PLUGIN_ROOT is a genuine legacy
first-time install and proceeds normally.) First check for a bare
preset: if $PRESET_ARG is non-empty AND $MODE is NOT install, jump to
## Bare-preset config-only short-circuit (config-only — no audit, no
pull, no fill) and exit there. Otherwise (no preset, or install <preset>)
proceed to Default Mode — Smart Detection / Fill-All-Gaps unchanged.
The dual case is intentionally NOT given its own arm — the mirror already
exists, so gap-fill is a non-destructive update, and the materialiser +
switch-install-path already own dual detection/warning/recovery;
/update-zskills must not add its own dual handling.
managed.md is not touched by this branch — because the plugin arm skips
the entire gap-fill (including Step B's render), the sentinel-clobber
landmine (a sentinel-less re-render shifting detect_install_state) cannot
occur on this path.
Runs on the legacy lane (i.e. when Step 0.7's Else arm was taken —
LANE is update-zskills, dual, or fresh, OR detection was
unreachable) when a bare preset was parsed: $PRESET_ARG is non-empty
AND $MODE is NOT install. (On the plugin lane the equivalent
config-only apply already happened in Step 0.7's preset arm, which then
prints the same confirmation + nudge documented here.)
A bare preset is a pure landing-mode switch — write the two
preset-owned config fields and STOP. Do NOT run the audit, do NOT
pull the source clone for skill updates, do NOT render/fill any gap.
The full smart-detect update is reserved for /update-zskills (no preset)
and /update-zskills install <preset>.
Step 1 — Apply the preset (config-only). Run the same
apply-preset.sh invocation as Step F (it edits ONLY
.claude/zskills-config.json — execution.landing +
execution.main_protected — preserving every other field; it never
touches the hook or the mirror):
if [ -f "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/scripts/zskills-resolve-config.sh" ]; then
export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
. "${CLAUDE_PLUGIN_ROOT}/skills/update-zskills/scripts/zskills-resolve-config.sh"
else
. "$CLAUDE_PROJECT_DIR/.claude/skills/update-zskills/scripts/zskills-resolve-config.sh"
fi
bash "$ZSKILLS_SKILLS_ROOT/update-zskills/scripts/apply-preset.sh" "$PRESET_ARG"
Capture the exit code per Step F's table (0 applied / 1 no-change / 2 usage / 3 missing config / 4 malformed config).
Step 2 — Config-only confirmation + version nudge. Print a confirmation that is EXPLICIT that nothing was pulled or updated, pointing the user at the bare no-arg command for a real update:
Applied preset <name> (config only — no skills pulled or updated).
(For exit 1 / no-change, say Preset <name> already applied — config unchanged (no skills pulled or updated). instead.)
Version nudge (best-effort, offline-safe — reuse the existing
machinery, do NOT invent new comparison logic). Reuse the SAME
resolve-repo-version + installed-zskills_version comparison the audit's
"Versions:" line uses (see ### Step 6 — Produce the gap report). The
ONLY addition for this path is a best-effort git fetch --tags on the
source clone FIRST, so the comparison sees newly-published tags that the
local clone has not yet fetched:
# Best-effort tag refresh on the source clone (offline-safe). The source
# clone is $ZSKILLS_PATH when Step 0 resolved a git clone; if it is unset,
# not a git repo, or the fetch fails (no network / no remote), SKIP the
# nudge entirely — NEVER fail or abort the config write because of it.
if [ -n "${ZSKILLS_PATH:-}" ] && [ -d "$ZSKILLS_PATH/.git" ]; then
git -C "$ZSKILLS_PATH" fetch --tags --quiet || true
fi
# Reuse the audit's resolve+read (single source of truth — same scripts):
current_zskills_ver=""
installed_zskills_ver=""
if [ -n "${ZSKILLS_PATH:-}" ]; then
current_zskills_ver=$(bash "$ZSKILLS_SKILLS_ROOT/update-zskills/scripts/resolve-repo-version.sh" "$ZSKILLS_PATH")
fi
if [ -f "$CLAUDE_PROJECT_DIR/.claude/zskills-config.json" ]; then
cfg=$(cat "$CLAUDE_PROJECT_DIR/.claude/zskills-config.json")
if [[ "$cfg" =~ \"zskills_version\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
installed_zskills_ver="${BASH_REMATCH[1]}"
fi
fi
Print the nudge line ONLY when the installed version is strictly behind
the latest available tag (both versions present AND
installed_zskills_ver sorts before current_zskills_ver under sort -V). When the fetch fails, there is no source clone, there are no tags,
the installed version is unknown, or installed ≥ current — silently skip
the nudge (print nothing extra; the config write already succeeded):
zskills <installed_zskills_ver> → <current_zskills_ver> available. Run /update-zskills (no args) to pull + update.
Step 3 — Exit. Exit with apply-preset.sh's exit code. Do NOT proceed
to the audit, Default Mode — Smart Detection, or any fill/update step.
Run the audit (Steps 1-6 above). Display the gap report.
Detect installation state:
.claude/skills/ directory exists, or it contains zero skills
-> treat as first-time install (proceed to "Fill All Gaps" below).This is also the path taken by the explicit install mode.
Run Step 0 (locate portable assets). If the path cannot be resolved, stop with an error: "Cannot locate zskills-portable/ directory. Please provide the path to the Z Skills source repo."
Target path: .claude/rules/zskills/managed.md in the project.
Create the .claude/rules/zskills/ subdirectory if absent. Claude Code
auto-loads everything under .claude/rules/ recursively at session
start, so no @-import from root ./CLAUDE.md is needed.
Ownership rule: zskills owns .claude/rules/zskills/ in full. The
user's root ./CLAUDE.md is theirs exclusively. No cross-writes:
Step B never reads or modifies root ./CLAUDE.md content (the
migration sub-step below is the sole, deterministic exception, and it
only removes zskills-rendered lines — never user content).
Render algorithm (every install, first-run and subsequent — idempotent):
Scan project files for auto-detected placeholder defaults (only used when the corresponding config field is empty):
package.json — name, scripts.start, scripts.dev, scripts.test,
scripts["test:all"], scripts["test:ci"]Cargo.toml — [package] namepyproject.toml / setup.py / setup.cfg — project name, test configMakefile — test, serve, dev targetsmanage.py — Django project (dev server: python manage.py runserver).github/workflows/ / .gitlab-ci.yml — CI test commandspytest.ini / jest.config.* / .mocharc.* — test framework detectionRender the template via the canonical renderer (D24 — one
substitution map, three callers; no LLM-prose substitution). First
resolve a working Python 3 (. "$PORTABLE/scripts/zskills-resolve-config.sh"
sets $PYTHON; on Windows a bare python3 is the broken MS Store stub —
#1083), then run:
"$PYTHON" "$PORTABLE/scripts/render-managed-rules.py" --config .claude/zskills-config.json --template "$PORTABLE/CLAUDE_TEMPLATE.md" --out .claude/rules/zskills/managed.md.
The renderer imports scripts/managed_rules_substitution.py (the single
source-of-truth build_substitutions + apply map), substitutes every
{{PLACEHOLDER}} from current .claude/zskills-config.json values
(empty dev_server.cmd / ui.auth_bypass / testing.file_patterns
render the documented TODO fallbacks), and exits non-zero rather than
ship a broken {{...}} token. This step both substitutes AND writes the
rendered content (step 3's write is performed by the renderer's
atomic-rename --out).
The renderer's --out writes .claude/rules/zskills/managed.md
atomically (step 2 performs the write). Full overwrite is safe by
ownership rule — zskills owns this file in full; no user content
ever lives here. The file is regenerated from template + config on
every install and every --rerender. The renderer never leaves broken
{{PLACEHOLDER}} strings (it exits non-zero instead).
Run the root-CLAUDE.md migration sub-step (below) to detect and
relocate any pre-Phase-4 zskills content from root ./CLAUDE.md.
Report:
.claude/rules/zskills/managed.md rendered. Values filled:
Project name: my-app (from package.json)
Dev server: npm start (detected)
Test command: npm test (detected)
Full test: commented out (no test:all script found — update when ready)
Review .claude/rules/zskills/managed.md and adjust config values if needed
(edit .claude/zskills-config.json, then rerun /update-zskills --rerender).
Migration sub-step — relocate pre-Phase-4 zskills content from root ./CLAUDE.md:
Earlier zskills installs rendered into root ./CLAUDE.md; Phase 4
moved the target to .claude/rules/zskills/managed.md. On every
install (first-run and subsequent), detect any zskills-rendered lines
still sitting in root ./CLAUDE.md and remove them — carefully, so
user-authored content that merely mentions a zskills value is
preserved. Idempotent: on a clean install or after a previous
migration, nothing matches and nothing changes.
Algorithm:
If root ./CLAUDE.md does not exist, the migration is a no-op.
Skip and continue.
Render the current template against current config (same
substitution used in Step B step 2 above) to produce a
$RENDERED_TEMPLATE string. This is the set of lines zskills would
write today.
For each placeholder P in CLAUDE_TEMPLATE.md whose current
rendered value V is non-empty, identify the set of lines in
$RENDERED_TEMPLATE that contain V. For each such "template
line," record its ±2-line neighbourhood in the template (2 lines
before, 2 lines after). The neighbourhood is the context
signature for that template line.
Walk root ./CLAUDE.md line by line. A root line is a migration
candidate iff:
V, AND./CLAUDE.md matches the
corresponding template line's context signature (line-for-line,
ignoring trailing whitespace).The context match restricts removal to lines that were genuinely
rendered by zskills. Prose that merely mentions a zskills value in
non-template context (e.g., "I remember we used to have
bash tests/run-all.sh…") fails the context check and is preserved.
If zero candidates, migration is a no-op. Do not create a backup, do not emit a NOTICE. Stop.
Otherwise: back up root ./CLAUDE.md to
./CLAUDE.md.pre-zskills-migration — only if that backup does
NOT already exist. Never overwrite a prior backup. This preserves
the user's pre-migration state across repeated /update-zskills
invocations.
Remove the matched candidate lines from root ./CLAUDE.md.
Everything else is left byte-identical. If the result is an empty
file, leave it as an empty file (do not delete) — an existing
./CLAUDE.md with no content signals "user chose zskills-only
rules and has no other project notes yet"; recreating it on next
invocation is cheaper than guessing intent.
Emit to stderr:
NOTICE: Migrated zskills content from root ./CLAUDE.md to .claude/rules/zskills/managed.md.
Backup: ./CLAUDE.md.pre-zskills-migration.
If your Claude Code settings exclude .claude/** from context (e.g. claudeMdExcludes),
the new rules file will not auto-load — adjust your excludes or @-import it from root CLAUDE.md.
NEVER modify user-authored content in root ./CLAUDE.md — the
migration removes only lines matching both value AND ±2-line template
context. Anything the user added (their own sections, notes,
references) is untouched.
Copy missing hooks from $PORTABLE/hooks/ to .claude/hooks/.
block-unsafe-project.sh.template: copy to
.claude/hooks/block-unsafe-project.sh. No install-time placeholder
fill needed — the hook reads testing.unit_cmd, testing.full_cmd,
and ui.file_patterns from .claude/zskills-config.json at runtime
via bash regex (same idiom as is_main_protected()). Just copy the
source template.block-unsafe-generic.sh, block-agents.sh.template,
warn-config-drift.sh: copy as-is. Wired into settings.json per
the canonical zskills-owned triples table below.inject-bash-timeout.sh: copy to
.claude/hooks/inject-bash-timeout.sh. No settings.json entry
— this hook is loaded via the .claude/agents/verifier.md
frontmatter hooks.PreToolUse declaration (Layer 0 timeout-injection
for verifier subagent Bash calls). Auto-discovered when the agent
definition is loaded at session start.verify-response-validate.sh: copy to
.claude/hooks/verify-response-validate.sh. No settings.json
entry — this hook is invoked directly from dispatcher SKILL.md
files (Layer 3 universal verifier-response failure-protocol
primitive). Standard executable shell script, called as
bash .claude/hooks/verify-response-validate.sh ... from skill
prose.block-stale-skill-version.sh: copy as-is from $PORTABLE/hooks/ to .claude/hooks/.block-bad-cron.sh: copy as-is from $PORTABLE/hooks/ to
.claude/hooks/. Wired into settings.json as PreToolUse on the
CronCreate matcher (see canonical-triples table below). Rejects
one-shot crons whose next-fire is in the past or > 7 days out
(closes a TZ-confusion class of "the cron never fired" bugs; PR #456).block-main-edits.sh: copy as-is from $PORTABLE/hooks/ to
.claude/hooks/. Wired into settings.json as PreToolUse on the
Edit|Write matcher (see canonical-triples table below).
Honors execution.main_protected from .claude/zskills-config.json —
denies edits to files inside the main repo working tree when
main_protected: true, with narrow allowlists for .zskills/ and
gitignored worktree-state markers (issue #308).block-fix-issue-unclaimed.sh: copy as-is from $PORTABLE/hooks/
to .claude/hooks/. Wired into settings.json as PreToolUse on the
Bash matcher (see canonical-triples table below). Backstops the
/fix-issues inline-acquire prose — denies any Bash invocation of
create-worktree.sh / ensure-worktree.sh whose resolved branch
matches fix-issue-NNN or fix/issue-NNN and lacks a matching
.zskills/claims/issue-NNN/ claim directory
(plans/fix-issues-claims.md Phase 2, W2.2c).block-run-plan-unclaimed.sh: copy as-is from $PORTABLE/hooks/
to .claude/hooks/. Wired into settings.json as PreToolUse on the
Bash matcher (see canonical-triples table below). Backstops the
/run-plan inline plan-claim-acquire prose — denies any Bash
invocation of create-worktree.sh / ensure-worktree.sh whose
resolved branch matches one of the three /run-plan shapes
(cp-<slug>, cp-<slug>-phase-<N>, or ${BRANCH_PREFIX}<slug> for
PR mode) and lacks a matching .zskills/plan-claims/<slug>/ claim
directory (plans/plans-claim-chip-parity.md Phase 2; PR #544).scripts/test-all.sh: copy as-is from
$PORTABLE/scripts/. Reads testing.unit_cmd from
.claude/zskills-config.json at runtime — no
install-time fill. (Tier-2 placeholder; consumer
customizes.){{E2E_TEST_CMD}}, {{BUILD_TEST_CMD}}): these have no config
source, so fill from project detection or leave as a # TODO
comment. Only these two placeholders — all others listed in the
Step 0.5 mapping table go through the template-render path (Step B),
not the hook path.The inject-bash-timeout.sh and verify-response-validate.sh hooks
have NO entries in the canonical zskills-owned triples table below
(they are not registered via settings.json). They are loaded via the
.claude/agents/verifier.md frontmatter PreToolUse declaration AND
via direct skill invocation in dispatcher SKILL.md files. Copy them to
.claude/hooks/ only — do not register them.
Refresh changed verbatim-owned hooks (#1060). The copy steps above
only fill missing hooks. To propagate a hook fix shipped after a
consumer's initial install (e.g. the session-log 0o600 security fix in
5148324), follow the missing-hook copy with a REFRESH pass over the
verbatim-owned hook set — the zskills-owned hooks that ship
byte-for-byte (no per-consumer fill). For each such hook, if the installed
copy differs from the source, BACK IT UP first (so a consumer who did
edit a verbatim hook can recover), then overwrite from source. This
mirrors the agent cmp -s cmp-and-overwrite loop below, plus a backup
step — hooks are higher-stakes than agents (which do not back up).
The verbatim-owned hook set is an EXPLICIT, CLOSED list — these are the
zskills-owned hooks that Step C copies as-is (the registered
settings.json triples that ship verbatim, plus the two frontmatter /
direct-invocation hooks, plus the two session-logging hooks). It MUST NOT
include the consumer-customizable .template-derived hooks
(block-unsafe-project.sh, block-agents.sh) or any Tier-2 stub under
scripts/ — those stay governed by Key Rule 2 (never overwritten):
# Verbatim-owned, zskills-shipped hooks — refreshed when stale. This is a
# CLOSED list; do NOT replace it with a glob/heuristic (a future
# consumer-owned hook must never be caught). EXCLUDES the .template hooks
# (block-unsafe-project.sh, block-agents.sh) and all Tier-2 scripts/ stubs.
VERBATIM_OWNED_HOOKS=(
block-unsafe-generic.sh
block-stale-skill-version.sh
block-bypassed-land-pr.sh
block-fix-issue-unclaimed.sh
block-run-plan-unclaimed.sh
block-bad-cron.sh
block-main-edits.sh
warn-config-drift.sh
inject-bash-timeout.sh
verify-response-validate.sh
log-session-stop.sh
log-permission-request.sh
)
for hook in "${VERBATIM_OWNED_HOOKS[@]}"; do
src="$PORTABLE/hooks/$hook"
dst=".claude/hooks/$hook"
[ -e "$src" ] || continue
[ -f "$dst" ] || continue # missing hooks are handled by the copy steps above
if ! cmp -s "$src" "$dst"; then
cp -a "$dst" "$dst.bak" # back up before overwrite (recoverability)
# Report old→new zskills-hook-version stamp where the hook carries one.
oldv=$(sed -n 's/^# zskills-hook-version:[[:space:]]*//p' "$dst" | head -1)
newv=$(sed -n 's/^# zskills-hook-version:[[:space:]]*//p' "$src" | head -1)
cp -a "$src" "$dst"
if [ -n "$oldv" ] || [ -n "$newv" ]; then
echo "Refreshed hook: $hook (${oldv:-?} -> ${newv:-?}; backup: $dst.bak)"
else
echo "Refreshed hook: $hook (backup: $dst.bak)"
fi
fi
done
Report each refreshed hook in the update summary (the Refreshed hook:
lines above), noting the old→new # zskills-hook-version: stamp where the
hook carries one.
Custom subagent definitions. After hook copy, copy missing or
changed agent definitions from $PORTABLE/.claude/agents/*.md to
$PROJECT_DIR/.claude/agents/. cp -a preserves mode bits + mtime.
The agent frontmatter references
$CLAUDE_PROJECT_DIR/.claude/hooks/inject-bash-timeout.sh — that path
is fixed, so the hook-copy step (above) is a hard prerequisite.
if [ -d "$PORTABLE/.claude/agents" ]; then
mkdir -p .claude/agents
for src in "$PORTABLE/.claude/agents"/*.md; do
[ -e "$src" ] || continue
name=$(basename "$src")
dst=".claude/agents/$name"
if [ ! -f "$dst" ]; then
cp -a "$src" "$dst" && echo "Installed agent: $name"
elif ! cmp -s "$src" "$dst"; then
cp -a "$src" "$dst" && echo "Updated agent: $name"
fi
done
echo "WARN: agent definitions auto-discover at session start. Restart Claude Code (or open a new session) before invoking verifier-using skills (/run-plan, /commit, /fix-issues, /do, /verify-changes). There is no in-session reload command."
fi
No settings.json wiring needed for agents. Auto-discovered.
Bash regex parse only — no jq. (Hook scripts may use Python;
inject-bash-timeout.sh does, per zskills convention exemption for
JSON round-trip.)
Consumer-customization handling: if a consumer edits
.claude/agents/verifier.md, the next install OVERWRITES with source
(idempotent cmp -s gate ensures only changed files are touched).
Document this expectation: agent definitions are framework-owned, not
consumer-owned. Consumers wanting to customize verification behavior
should compose around the agent (wrap dispatch in their own skill),
not edit the agent definition file in place.
Note: hooks and helper scripts read testing.*, ui.file_patterns,
and dev_server.main_repo_path from .claude/zskills-config.json at
runtime. No install-time fill needed. Only copy the source template.
Explain what each hook does so the user understands what's being added:
Installing 3 PreToolUse Bash safety hooks (commit-time + pre-tool-execution gates):
- block-unsafe-generic.sh — blocks destructive commands (
git reset --hard,rm -rf,kill -9,git checkout --,--no-verify, etc.) and discipline violations (git add .).- block-unsafe-project.sh — project-specific guards: prevents piping test output (must capture to file), verifies tests ran before commit, optionally checks for UI verification before committing UI changes, enforces tracking discipline.
- block-stale-skill-version.sh — denies
git commitwhen staged skill files have a stalemetadata.versionhash; reusesscripts/skill-version-stage-check.sh.See the canonical table below for the full hook set (additionally: PreToolUse
Agentmatcher →block-agents.sh; PostToolUseEdit/Writematchers →warn-config-drift.sh).
Main-push block (config-derived): block-unsafe-generic.sh
blocks git push to main/master when BLOCK_MAIN_PUSH=1 — a value the
hook derives at runtime from execution.main_protected in
.claude/zskills-config.json (fail-closed: defaults to 1/block when the
config is absent or unreadable). The preset sets main_protected in config
(Step F is config-only); the hook reads it. No hook edit here in Step C.
Effective mapping: cherry-pick/direct → main_protected:false → allow
push; locked-main-pr → main_protected:true → block push.
Note on tracking enforcement: The tracking enforcement section in
block-unsafe-project.sh (protecting .zskills/tracking/, blocking
clear-tracking.sh execution, and enforcing delegation/step verification)
has no placeholders — it works out of the box. No configuration needed.
Add tracking directory to .gitignore: During installation, add
.zskills/tracking/ to the project's .gitignore if not already present.
Tracking files are ephemeral session state and should never be committed.