Skip to content
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.

From plugin
ai-sdlc-harness
198 skills3 agents4 hooks
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

  • MatchesBash|run_shell_commandexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" bash || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard bash
  • MatchesWrite|WriteFile|write_file|Edit|MultiEdit|NotebookEdit|notebook_exec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" write || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard write
  • MatchesAgent|Taskexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" spawn || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard spawn
  • MatchesSkillexec "${CLAUDE_PLUGIN_ROOT}/hooks/run-guard" skill || ${CLAUDE_PLUGIN_ROOT}/hooks/run-guard skill
  • MatchesRead|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

  • MatchesAgent|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
Read hooks/hooks.json

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
Stats
19
Stars
4
Forks
Active
Maintenance
Python
Language
MIT
License
26d ago
Last commit
4mo ago
Created

Repo: MostAshraf/ai-sdlc-harness