Development
Hook
Hooks
What ai-sdlc-harness runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add MostAshraf/ai-sdlc-harness > /plugin install ai-sdlc-harness@ai-sdlc-harness
Ships with ai-sdlc-harness. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
Bash|run_shell_commandexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" bash || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard bash - Matches
Write|WriteFile|write_file|Edit|MultiEdit|NotebookEdit|notebook_exec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" write || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard write - Matches
Agent|Taskexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" spawn || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard spawn - Matches
Skillexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" skill || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard skill - Matches
Read|ReadFile|read_file|Grep|grep_searchexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" read || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard read
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
exec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" user-prompt || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard user-prompt
PostToolUse
- Matches
Agent|Taskexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" post-spawn || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard post-spawn
SubagentStop
exec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" subagent-stop || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard subagent-stop
Where it lives
- hooks/guards.pyGitHub
Read the script
#!/usr/bin/env python3 """Plugin guard layer (design.md piece 3 + RC1/RC4 + invocation control). One dispatcher, selected by argv[1] — registered in hooks/hooks.json. Exit 0 = allow · exit 2 = block (stderr is the redirect-to-`harness` message). Per-guard policy (declared, a "keeping" from the original): bash fail-open on unparseable payload — the HMAC chain (RC4) is the guarantee for authority files; this guard is fast-fail. Its raw-commit/merge/rebase/.../push block (GIT_VERB_RE) is a STANDING, workspace-scoped invocation rule, not a run-state guard: it applies for the life of a harness workspace — from the moment `/init-workspace` completes, regardless of whether any `ai/<run>/` currently exists — unlike `spawn` below, there is no "no run yet" carve-out for it (adversarial-review finding: previously undocumented, easy to mistake for a run-scoped check like the others in this list). It is NOT session- or repo-wide beyond that: `_is_harness_workspace` gates it on the `/init-workspace` bootstrap marker, so a session touching an unrelated, never-initialized repo sees raw git untouched — see `_is_harness_workspace` for the one documented residual this leaves open. write fail-open on unparseable payload; fail-closed on authority paths (they are never legal via tools). spawn FAIL-CLOSED: no run -> no harness-shape spawns beyond the declared out_of_run exceptions; integrity failure blocks spawn-legality from JUST the corrupt run, not the rest of the workspace (harness reseal is the recovery verb). skill fail-closed for user-entry skills from subagent context. user-prompt never blocks (capture only). subagent-stop never blocks (capture + stall detection feed events ledger). If PyYAML is missing, the yaml-needing guards DEGRADE OPEN with one visible remediation line on stderr (exit 0 — see main()'s YamlMissing handler; the yaml-free bash/write guards keep blocking); init-workspace verifies the dependency up front, and the HMAC chain (RC4) still detects authority-file tampering even with guards down — defense in depth, guard = fast-fail, chain = guarantee. The same posture covers a missing INTERPRETER: if the hook launcher pair (hooks/run-guard + run-guard.cmd, registered in hooks.json) finds no runnable python at all — including the Windows Store alias that answers to `python`/`python3` but only prints an install nag — the hook errors non-2 and the platform treats it as non-blocking. Accepted: pre-venv, nothing harness-y can execute anyway. """ from __future__ import annotations import hashlib import json import os import re import sys import tempfile from pathlib import Path PLUGIN_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PLUGIN_ROOT)) # stdlib-only modules at import time. `transitions` qualifies: it imports # nothing but json/sys/pathlib plus these two, and reaches for PyYAML only # lazily, inside the declared-data readers this file calls from paths that # have already loaded surfaces.yaml (i.e. already required it). `gates` # qualifies outright — hashlib + re, no I/O, no env — and is imported for # `session_digest` alone, so the capture hook and the CLI that writes the # stamp cannot drift into two different hashes of the same session id. from harness import chain, gates, ndjson, transitions # noqa: E402 class YamlMissing(Exception): pass def load_yaml(path: Path): """Lazy YAML — only the spawn/skill guards need it. The bash/write/capture guards are pure regex+payload and must keep working (and blocking!) on a Python without PyYAML, e.g. macOS system python3 before setup.""" try: import yaml except ImportError: raise YamlMissing( "ai-sdlc-harness: PyYAML missing for this hook's interpreter — " "/init-workspace bootstraps the plugin venv; until then this " "guard degrades open.") from None with path.open(encoding="utf-8") as fh: return yaml.safe_load(fh) _TERMINAL_CACHE: tuple = () def _terminal_statuses() -> tuple: """The task FSM's declared terminal statuses — the SAME `terminal:` list `harness.transitions.terminal_statuses` reads, not a second copy. A guard that disagreed with the engine about what "finished" means would refuse a spawn the engine considers legal, or wave one through that it doesn't.""" global _TERMINAL_CACHE if not _TERMINAL_CACHE: fsm = load_yaml(PLUGIN_ROOT / "pipeline" / "task-fsm.yaml") or {} _TERMINAL_CACHE = tuple(fsm.get("terminal") or ()) return _TERMINAL_CACHE # A single "word" a flag's value can take — a whole quoted string (its # space(s) included) counts as ONE token, not just up to the first # whitespace (adversarial-review round 3 finding: plain `\S+` matched only # `-C "my` out of `-C "my repo" commit`, leaving `repo" commit` unable to # reach the verb — silently reopening the bypass for any quoted, # space-containing flag value). Round 4 finding: a token can also be a MIX # of bare and quoted segments the way the shell itself tokenizes — # `-c user.name="My Name"` is ONE shell word, but the round-3 alternation # (whole-quoted OR \S+) consumed only `user.name="My` and the parse died # before the verb, reopening the same bypass one level down. One-or-more # runs of (quoted segment | bare segment) matches shell word semantics. _GIT_TOKEN = r"""(?:"[^"]*"|'[^']*'|[^\s"'|;&])+""" # `git` not immediately preceded by a quote char (adversarial-review round # 3 finding): without this, `grep -rn 'git reset --hard' .` — a pure read, # searching for the LITERAL PHRASE — blocked, because "git reset --har
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withai-sdlc-harness
A governed multi-agent SDLC pipeline for Claude Code and Qwen Code — a ground-up rewrite of ai-sdlc-harness.
Get the whole plugin

