Init Pipeline
Scaffold pipeline enforcement infrastructure into the current project: Claude Code hooks for skill compliance, git guardrails for safety, and pre-commit hooks for code quality. Detects existing tools before suggesting defaults.
When to use
- Automatically invoked by
/executeStep 0 if.claude/hooks/enforce-classification.shis missing, or if the installed hook predates the post-review edit lock (see § 2's re-scaffold note) - Manually by the user when setting up a new project for pipeline work
What it sets up
All files are created in the target project, not in the Skill Kit repo.
1. Git guardrails (Claude Code hook)
Invoke /git-guardrails-claude-code with project scope.
This installs a PreToolUse hook on Bash that blocks destructive git commands — force pushes, hard resets, forced cleans, force branch deletes, and whole-tree checkout/restore. The authoritative list lives in /git-guardrails-claude-code's "What Gets Blocked" and "What Stays Allowed" sections; read it there rather than here.
The list is deliberately not restated in this file. It was, and the copy was wrong: it claimed plain git push was blocked, which it never has been. That is the same defect as #227 — documentation asserting coverage the matcher does not have — and a second copy is a second thing to keep true, in the file more downstream projects actually read.
2. TDD classification gate (Claude Code hook)
Create .claude/hooks/enforce-classification.sh and make it executable. This blocks Write/Edit to implementation files unless the /execute Step 3 classification gate has been passed.
The hook checks for either .claude/.tdd-active (TDD invoked) or .claude/.tdd-skipped (visual frontend, explicitly opted out). No path checking beyond the trigger surface — it enforces "did you go through the gate?"
It carries a second clause on the same trigger surface: the post-review edit lock, which refuses an implementation write while .claude/.review-stamped exists and .claude/.fix-findings-active does not. The first clause asks "was this work classified?"; the second asks "is this edit landing after a review, authored by the session the review went around?" Both are one script because both key off the same file-pattern decision.
The two clauses are ordered, not independent — read the .review-stamped term in the first one before editing either. The classification clause stands down on a stamped branch, so the post-review clause is the only one that decides there. This is not a stylistic preference: /execute Step 6 removes .claude/.tdd-active and .claude/.tdd-skipped before it hands off to /pre-merge, so a stamped branch never carries a classification marker. Two independent clauses in this order therefore never reach the second one — the /fix-findings fixer is refused despite holding the flag written for it, and the authoring session is refused by the wrong clause, told to invoke /tdd and never told that /fix-findings is the route. A lock whose designed affordance never prints is a lock nobody can use, and it was described here as "independent" while behaving this way. scripts/test-post-review-edit-lock.sh now drives /execute Step 6's removal as part of the round trip, so the ordering is measured rather than asserted.
Install-time: the trigger surface is settled on one of three paths, and at most one of them applies to a given run. Read .claude/hooks/enforce-classification.sh in the target project before writing anything. What is already installed there decides the path — not how this skill was invoked, because /execute Step 0 routes both of its verdicts (hooks-absent and hooks-stale) to the same auto-invocation. Take no instruction from a path you are not on.
Path A — no hook installed, and a user is present. Present the default include list and ask:
"The TDD classification gate fires on Write/Edit of files matching a pattern list. Default:
*.ts, *.tsx, *.astro, *.py, *.go, *.rb, *.java, *.rs, *.js, *.jsx, *.vue, *.svelte. Over-gating is acceptable — classification is a quick decision at the top of /execute, though backend/behavior-heavy matches will trigger a full /tdd cycle. Accept the default, or customize for this project?"
Use the confirmed list (default or customized) to populate the IMPL_PATTERNS array in the hook body below. Over-gating is acceptable — the cost of an extra classification prompt is lower than the cost of silent under-fire on a polyglot project.
Path B — no hook installed, and /init-pipeline is running non-interactively (auto-invoked by /execute Step 0 on a project whose hooks gate reported hooks-absent). Do not ask. Populate IMPL_PATTERNS with the default list from Path A's question and record that fact in the hook body via a leading comment.
Path C — a hook is installed but does not read .claude/.review-stamped (hooks-stale: every project initialized before the post-review edit lock shipped). The trigger-surface question was answered at that project's own install, so neither Path A's question nor Path B's default applies here — both belong to the fresh-install case, and reaching for either on this path is the one harm this path exists to prevent. Do not ask again, and do not silently re-default: carry the installed hook's IMPL_PATTERNS array — with the provenance comment above it — and its skip clauses over verbatim, because the project may have customized them at its own install time, and changing them changes the classification gate's behavior for work that has nothing to do with the lock. Replace everything from the installed hook's # Check for classification markers comment through its final exit 0 with the marker-check clauses at the foot of the block below — that is one clause replaced and one added, not two replaced, because a pre-lock hook carries the classification clause alone and the post-review clause is what this upgrade brings.
The rest of the run, per path. § 4 and § 5 are the two sections below that wait on a human — § 4 presents its detection findings and holds for a confirmation before invoking /setup-pre-commit, and § 5 asks whether to install the optional quality gate — and a run with nobody to ask cannot execute either. That is not hypothetical: /execute Step 0 sends both of its verdicts here and holds Step 1 until this skill reports, so a section waiting on an answer nobody is present to give stalls the caller rather than the callee, and an AFK Ralph iteration drives claude --message with no user at the keyboard at all. On Path A, run everything below, § 4 and § 5 included. On Path B, run § 3, § 6 and § 7; take § 4 without asking, applying what its detection block finds or the default it names when it finds nothing; and skip § 5, which is optional and whose unanswered default is no. On Path C, run § 3 (a merge, never an overwrite) and § 6 — whose .gitignore append is idempotent, and is the step that stops the lock's two flags from landing as committable untracked files in a repo that predates them — then stop: skip § 4, § 5 and § 7 outright, because this project answered them at its own install and a lock upgrade that re-opens a settled toolchain decision is doing something nothing asked it to do. If a Path C run notices that a section it skipped never ran at that project at all, say so in what it reports and leave it — /init-pipeline invoked by hand, with a user present, is Path A and asks properly.
Skip logic stays extension-agnostic. Tests, type declarations, and config files are detected by path substring (*test*, *spec*, .d.ts, .config.*) rather than per-language expansion.
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Implementation patterns — populated at install time from the user's answer to
# the /init-pipeline trigger-surface question. Over-gating is intended.
IMPL_PATTERNS=("*.ts" "*.tsx" "*.astro" "*.py" "*.go" "*.rb" "*.java" "*.rs" "*.js" "*.jsx" "*.vue" "*.svelte")
MATCHED=0
for pattern in "${IMPL_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == $pattern ]]; then MATCHED=1; break; fi
done
if [ $MATCHED -eq 0 ]; then exit 0; fi
# Skip test files and type declarations (extension-agnostic)
if [[ "$FILE_PATH" == *test* || "$FILE_PATH" == *spec* || "$FILE_PATH" == *.d.ts ]]; then
exit 0
fi
# Skip config files (drizzle.config, vite.config, etc.)
if [[ "$FILE_PATH" == *.config.* ]]; then
exit 0
fi
# Check for classification markers — but stand down on a stamped branch, so the
# post-review clause below is the one that decides there. /execute Step 6 removes
# BOTH classification markers before it hands off to /pre-merge, so by the time
# .review-stamped exists there is never a marker left for this test to find.
# Without the .review-stamped term, this clause short-circuits every post-review
# write: the /fix-findings fixer is refused outright, and the authoring session is
# refused by the wrong clause, under a message that names /tdd and never names the
# route the lock was built to offer.
if [ ! -f "$CLAUDE_PROJECT_DIR/.claude/.review-stamped" ] && [ ! -f "$CLAUDE_PROJECT_DIR/.claude/.tdd-active" ] && [ ! -f "$CLAUDE_PROJECT_DIR/.claude/.tdd-skipped" ]; then
echo '{"decision":"block","reason":"BLOCKED: classify work in /execute Step 3 before writing implementation files. Either invoke /tdd (backend/behavior-heavy) or create .claude/.tdd-skipped (visual frontend)."}' >&2
exit 2
fi
# Post-review edit lock. /pre-merge Phase 4 touches .review-stamped beside the
# review-currency stamp; /fix-findings touches .fix-findings-active when it
# loads and removes it when it reports. Between those two, an implementation
# edit is a post-review fix authored by the session the review just went around.
if [ -f "$CLAUDE_PROJECT_DIR/.claude/.review-stamped" ] && [ ! -f "$CLAUDE_PROJECT_DIR/.claude/.fix-findings-active" ]; then
echo '{"decision":"block","reason":"BLOCKED: this branch has been reviewed and stamped. A post-review fix needs an independent author. Either invoke /fix-findings <numbers>, or delete .claude/.review-stamped to take the edit yourself."}' >&2
exit 2
fi
exit 0
The two routes the refusal names, and what the clause actually refuses.
Invoking /fix-findings routes the edit to a sub-agent that did not write the
code; deleting .claude/.review-stamped by hand takes the edit anyway. That
second choice is made silently today, and printing it is what makes it visible.
It is not the only way out, and the difference matters at install time: the hook
is registered in § 3 below on "matcher": "Write|Edit", so no Bash write is
ever presented to it — sed -i, tee, a heredoc, printf >, or rm .claude/.review-stamped itself — and within Write/Edit it refuses only a path
that matches IMPL_PATTERNS and survives the skip logic, which leaves
.claude/.fix-findings-active, this hook script, and .claude/settings.json
all writable while the lock is armed. Install it as a stop on the default
post-review edit, not as an enclosure around the branch.
What the clause does not cover, in the matcher's terms rather than a file's
role. It reuses the IMPL_PATTERNS list and the *test* / *spec* /
*.d.ts / *.config.* skip logic above, unchanged, and those patterns match a
substring of the whole path. Claude Code hands the hook an absolute path, so
the skips reach further than the file roles they were named for:
src/latest-news.ts and src/respectful.ts are skipped for containing test
and spec, so is anything under a testimonials/ directory, so is
src/app.config.local.ts, and so is every file in a project checked out beneath
a path like /Users/tester/. Giving the two clauses different definitions of
"implementation file" would make the hook's behavior unreadable from its own
source, so the patterns stay — a re-run of /pre-merge, not this hook, covers
the edits they let through.
After writing, run: chmod +x .claude/hooks/enforce-classification.sh
3. Claude Code settings
Create or merge .claude/settings.json with both hooks:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/enforce-classification.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-git.sh"
}
]
}
]
}
}
If .claude/settings.json already exists, merge the hooks.PreToolUse entries — do not overwrite existing settings.
4. Pre-commit hooks and package manager enforcement
Gated by § 2's rule for the rest of the run, per path — read it before asking anything here.
Detect before suggesting. Before invoking any setup, check what the project already uses:
# Package manager — check lockfiles
ls pnpm-lock.yaml package-lock.json yarn.lock bun.lockb 2>/dev/null
# Formatter/linter — check deps and configs
grep -E "biome|prettier|eslint|oxlint|dprint" package.json 2>/dev/null
ls biome.json biome.jsonc .prettierrc .prettierrc.* prettier.config.* .eslintrc* eslint.config.* 2>/dev/null
# Hook manager — check for existing setup
ls lefthook.yml .husky 2>/dev/null
Present findings to the user and ask for confirmation:
- If a formatter/linter is detected → "Found [tool]. I'll use it for pre-commit hooks."
- If none detected → "No formatter/linter found. I'd suggest Biome (handles both formatting and linting, fast, zero-config). Want Biome, or something else?"
- If a hook manager is detected → "Found [Lefthook/Husky]. I'll use it." (Suggest migrating Husky to Lefthook if Husky is found.)
- If none detected → "No hook manager found. I'd suggest Lefthook. OK?"
- If a package manager lockfile is detected → use that package manager
- If none detected → "No lockfile found. I'd suggest pnpm. OK?"
After user confirms, invoke /setup-pre-commit with the confirmed tools.
Recommended Lefthook + Biome config (when both are confirmed):
# Pre-push hook for vitest run should be added after test suite stabilizes.
pre-commit:
commands:
check:
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
stage_fixed: true
Key flags: --no-errors-on-unmatched prevents false failures when no staged files match, --files-ignore-unknown=true avoids Biome choking on unsupported files, --colors=off gives cleaner hook output. Skip typecheck in pre-commit (too slow) — add it as a pre-push hook later.
Package manager enforcement: If the confirmed package manager is pnpm (detected or chosen), add the only-allow guard:
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
For npm or yarn, skip this step — only-allow is only needed when enforcing pnpm specifically.
5. Quality gate (Claude Code hook — optional)
Gated by § 2's rule for the rest of the run, per path — read it before asking anything here.
Ask the user: "Do you want a quality gate hook that runs feedback loops during editing? This catches issues while Claude works, not just at commit time."
If yes, create .claude/hooks/quality-gate.sh and make it executable. This runs as a PostToolUse hook on Write|Edit, providing immediate feedback after each file change. The first thing it does is anchor to $CLAUDE_PROJECT_DIR and no-op if that directory is gone — a hook firing from a worktree that was just torn down (e.g. during /closeout) must not emit false MODULE_NOT_FOUND errors from a vanished node_modules.
Detect available feedback loops first. Check package.json scripts for check/lint, tsc/typecheck, and test/vitest. Only include loops that actually exist.
#!/bin/bash
# Quality gate — runs after each Write/Edit to catch issues early.
# Only runs on TypeScript/JavaScript files. Skips test/config files.
# Anchor to the project root before running any feedback loop. If the
# directory is gone — e.g. a worktree was removed out from under the
# shell during /closeout teardown — no-op instead of emitting false
# MODULE_NOT_FOUND errors from a vanished node_modules. A stranded cwd
# must never masquerade as a lint/type failure.
if [ -z "$CLAUDE_PROJECT_DIR" ] || [ ! -d "$CLAUDE_PROJECT_DIR" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only gate TypeScript/JavaScript implementation files
if [[ ! "$FILE_PATH" == *.ts && ! "$FILE_PATH" == *.tsx && ! "$FILE_PATH" == *.js && ! "$FILE_PATH" == *.jsx ]]; then
exit 0
fi
# Skip test files, type declarations, config files
if [[ "$FILE_PATH" == *test* || "$FILE_PATH" == *spec* || "$FILE_PATH" == *.d.ts || "$FILE_PATH" == *.config.* ]]; then
exit 0
fi
# 1. Biome check (fast — format + lint)
BIOME_OUTPUT=$(pnpm biome check src/ 2>&1)
BIOME_EXIT=$?
# 2. TypeScript type check
TSC_OUTPUT=$(pnpm tsc --noEmit 2>&1)
TSC_EXIT=$?
if [ $BIOME_EXIT -ne 0 ] || [ $TSC_EXIT -ne 0 ]; then
if [ $BIOME_EXIT -ne 0 ]; then
echo "Biome errors found:" >&2
echo "$BIOME_OUTPUT" >&2
echo "" >&2
fi
if [ $TSC_EXIT -ne 0 ]; then
echo "TypeScript errors found:" >&2
echo "$TSC_OUTPUT" >&2
fi
exit 2
fi
# 3. Run tests for changed files only (vitest import graph analysis)
VITEST_OUTPUT=$(pnpm vitest run --changed 2>&1)
VITEST_EXIT=$?
if [ $VITEST_EXIT -ne 0 ]; then
echo "Tests failed for changed files:" >&2
echo "$VITEST_OUTPUT" >&2
exit 2
fi
exit 0
Project-specific extensions: If the project has domain-specific smoke tests (e.g., RAG agent tests, API health checks), append them after the generic checks. Use git diff --name-only HEAD to scope them to relevant directories.
After writing, run: chmod +x .claude/hooks/quality-gate.sh
Add the PostToolUse hook to .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/quality-gate.sh"
}
]
}
]
}
}
Merge into existing settings — do not overwrite.
6. .gitignore additions
Append these lines if not already present:
.claude/.tdd-active
.claude/.tdd-skipped
.claude/.ralph-checked
.claude/.review-stamped
.claude/.fix-findings-active
.claude/.ralph-checked is reserved here but created by /setup-ralph-loop, which is auto-invoked by /execute when a multi-slice task needs AFK bounds or may be run manually. /init-pipeline does not create the marker itself.
.claude/.review-stamped and .claude/.fix-findings-active are the post-review edit lock's two flags, also reserved here rather than created: /pre-merge Phase 4 writes the first beside the review-currency stamp and /closeout removes it at merge; /fix-findings writes the second when it loads and removes it when it reports. Both are transient, and a committed one would hold the lock open or shut across every future branch.
7. Worktree provisioning mode (optional)
/execute Step 0 and /closeout consult an optional .claude/settings.json key, worktree.provisioning, to decide whether the pipeline owns worktree provisioning and teardown or whether the host environment does. It mirrors the existing research.storage precedent — a single, in-repo, authoritative representation of an environment fact (Hunt/Thomas, DRY) rather than env-sniffing scattered across skills.
"auto"(default when the key is absent) —/executestands down if a host env var is present (CONDUCTOR_WORKSPACE_PATH,CODESPACES,REMOTE_CONTAINERS) or the current tree is not the repo's primary working tree; otherwise it provisions via worktrunk or plain git."host"— isolation is always host-owned./executeworks in place;/closeoutmerges but cedes worktree teardown and branch pruning to the host."pipeline"— the pipeline always provisions and tears down (the pre-host behavior).
{
"worktree": {
"provisioning": "host"
}
}
Scaffold host when running inside a host environment. If /init-pipeline runs while a host env var is set, write worktree.provisioning: "host" explicitly — the env var that disambiguates is available at scaffold time, and an explicit setting is more robust than re-deriving it on every /execute:
if [ -n "$CONDUCTOR_WORKSPACE_PATH" ] || [ -n "$CODESPACES" ] || [ -n "$REMOTE_CONTAINERS" ]; then
: # merge {"worktree":{"provisioning":"host"}} into .claude/settings.json
fi
Merge into existing settings — do not overwrite. When no host env var is present, leave the key unset (auto is the safe default for un-hosted repos, where the pipeline should provision).
Verification
Before considering setup complete, check — against the sections § 2's per-path rule actually sent this run through, not against all seven. A Path C re-scaffold produces § 2's hook body, § 3's merge and § 6's append and nothing else, so the pre-commit and quality-gate items below belong to the install that already ran and are not this run's to satisfy:
-
.claude/hooks/enforce-classification.shexists and is executable - Hook's
IMPL_PATTERNSarray matches the project's implementation surface: on § 2's Path A or B, the default list or the customized answer confirmed during this install; on Path C, byte-identical to the array in the hook that was already there -
.claude/hooks/block-dangerous-git.shexists and is executable -
.claude/settings.jsonhas both PreToolUse hooks configured - If quality gate accepted:
.claude/hooks/quality-gate.shexists, is executable, and PostToolUse hook is in settings - Hook manager config exists (e.g.
lefthook.yml) - Pre-commit hooks run successfully
-
.gitignorehas marker entries - If running inside a host environment (Conductor/Codespaces/devcontainer),
.claude/settings.jsonhasworktree.provisioning: "host"; otherwise the key is left unset (auto) - No existing project settings were overwritten
Handoff
- Expected input: any project that will use
/execute - Produces: complete enforcement infrastructure — Claude Code hooks, git guardrails, pre-commit hooks using detected or user-confirmed tools
- Auto-invoked by:
/executeStep 0 when.claude/hooks/enforce-classification.shis missing, or when it exists but does not read.claude/.review-stamped— a pre-lock install, re-scaffolded per § 2 - Invokes:
/git-guardrails-claude-code(project scope),/setup-pre-commit - Supports downstream:
/tdd(marker creation),/execute(marker cleanup); reserves.claude/.ralph-checkedfor/setup-ralph-loop, which creates the marker itself (auto-invoked by/executefor multi-slice work, or run manually); reserves.claude/.review-stampedand.claude/.fix-findings-activefor the post-review edit lock, written by/pre-mergePhase 4 and/fix-findingsrespectively