Use Claude Code hooks as deterministic workflow enforcement — block-then-instruct pattern, CLI command surfaces for hook interception, when to prefer hooks over CLAUDE.md/memory
Scanned 9/2/2026
Install to Claude Code
npx -y skills add kookr-ai/kookr --skill hook-driven-workflow-enforcement --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Hook Driven Workflow Enforcement?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kookr-ai-hook-driven-workflow-enforcement)More formats (shields.io, HTML) on the badges page.
---
name: hook-driven-workflow-enforcement
description: Use Claude Code hooks as deterministic workflow enforcement — block-then-instruct pattern, CLI command surfaces for hook interception, when to prefer hooks over CLAUDE.md/memory
triggers:
- enforce workflow
- deterministic
- hook enforcement
- agent compliance
- pre-PR
- post-PR
- block command
- workflow gate
- CLI command surface
- agent won't follow
- agent skips
- not following instructions
---
# Hook-Driven Workflow Enforcement
## Core Insight
There are two fundamentally different ways to influence agent behavior:
| Mechanism | Determinism | How it works | Failure mode |
|-----------|-------------|--------------|--------------|
| CLAUDE.md / memory / skills | **Non-deterministic** | Agent reads instructions, may or may not follow | Agent skips, forgets, or deprioritizes |
| Hooks (PreToolUse / PostToolUse) | **Deterministic** | System intercepts tool calls, blocks or injects feedback | Cannot be bypassed — the hook runs regardless of agent intent |
**Rule of thumb:** If an agent must *always* do X before Y, use a hook. If it *should* do X, use CLAUDE.md. Hooks are for invariants, instructions are for guidelines.
## How It Works
```mermaid
sequenceDiagram
participant Agent
participant Claude Code
participant PreToolUse Hook
participant State File
participant PostToolUse Hook
Note over Agent: Agent wants to run<br/>gated command (e.g. gh pr create)
Agent->>Claude Code: Bash("gh pr create ...")
Claude Code->>PreToolUse Hook: stdin: {tool_name, tool_input}
PreToolUse Hook->>State File: Check: /tmp/.gate-<branch>-done exists?
State File-->>PreToolUse Hook: No
PreToolUse Hook-->>Claude Code: {"decision":"block","reason":"Run /pre-pr-review first"}
Claude Code-->>Agent: ❌ Blocked + reason shown as system-reminder
Note over Agent: Agent follows instructions,<br/>runs prerequisite skill
Agent->>Claude Code: Skill("pre-pr-review")
Note over Claude Code: Skill runs typecheck,<br/>tests, diff review...
Claude Code-->>Agent: ✅ Skill complete
Agent->>Claude Code: Bash("touch /tmp/.gate-<branch>-done")
Claude Code->>State File: Create marker
Note over Agent: Agent retries gated command
Agent->>Claude Code: Bash("gh pr create ...")
Claude Code->>PreToolUse Hook: stdin: {tool_name, tool_input}
PreToolUse Hook->>State File: Check: exists?
State File-->>PreToolUse Hook: Yes
PreToolUse Hook-->>Claude Code: (exit 0, no output = allow)
Claude Code->>Agent: ✅ Command executes
Claude Code->>PostToolUse Hook: stdin: {tool_name, tool_input, tool_response}
PostToolUse Hook-->>Claude Code: "Now verify checklist, monitor CI..."
Claude Code-->>Agent: Post-step instructions shown
PostToolUse Hook->>State File: rm marker (cleanup)
```
## State Lifecycle
```mermaid
stateDiagram-v2
[*] --> No_Marker: Agent session starts
No_Marker --> Blocked: Agent calls gated command
Blocked --> No_Marker: Agent reads block reason
No_Marker --> Prereq_Running: Agent runs prerequisite skill
Prereq_Running --> Marker_Exists: Skill succeeds, creates marker
Marker_Exists --> Allowed: Agent retries gated command
Allowed --> No_Marker: PostToolUse hook cleans up marker
Marker_Exists --> Stale: Session crashes / agent abandoned
Stale --> No_Marker: TTL expires (1 hour max age)
note right of Blocked
Hook returns JSON:
{"decision":"block","reason":"..."}
Agent sees reason as system-reminder
end note
note right of Marker_Exists
File: /tmp/.gate-{branch}-done
Branch-scoped to avoid
cross-agent interference
end note
note right of Stale
Cleanup: hook checks file age
if older than TTL, ignores it
and re-blocks
end note
```
## Pattern 1: Block-Then-Instruct
The most powerful pattern for enforcing pre-step workflows.
### Critical: Hook Blocking API
Two JSON formats are validated for blocking PreToolUse commands. Both exit 0.
```bash
# ✅ Format A — short form (used by oss-contribution-gate)
echo '{"decision":"block","reason":"Must run prereq first."}'
exit 0
# ✅ Format B — hookSpecificOutput (used by pr-workflow-gate)
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Must run prereq first."}}'
exit 0
# ✅ Allow — no output, exit 0
exit 0
# ❌ WRONG — exit non-zero causes a hook error, not a clean block
echo "Must run prereq first."
exit 2
```
Both formats produce the same result: the command is blocked and the reason text appears to the agent as a `<system-reminder>` that cannot be ignored.
Empirically validated on 2026-04-01 (both formats tested live).
### Implementation skeleton
```json
// In settings.json hooks section
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "/path/to/gate.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "/path/to/post.sh" }]
}
]
}
}
```
### State marker management
- **Create marker**: The pre-step skill/command creates it on success: `touch /tmp/.gate-$(git branch --show-current)-done`
- **Scope per branch**: Include branch name to avoid cross-agent interference
- **Cleanup**: Remove after command succeeds (in PostToolUse hook): `rm -f "$STATE_FILE"`
- **TTL**: Ignore markers older than 1 hour to prevent stale state from crashed sessions:
```bash
if [ -f "$STATE_FILE" ]; then
AGE=$(( $(date +%s) - $(stat -c %Y "$STATE_FILE" 2>/dev/null || echo 0) ))
if [ "$AGE" -gt 3600 ]; then
rm -f "$STATE_FILE" # stale — re-gate
else
exit 0 # valid — allow
fi
fi
```
---
## Example 1: Pre-PR Review Gate (Simple)
The most common use case — ensure agents run quality checks before creating pull requests.
**Problem**: Agents skip type-checking, tests, and diff review before `gh pr create` despite CLAUDE.md saying to do it.
**Solution**: A PreToolUse hook blocks `gh pr create` until the `/pre-pr-review` skill has run.
### Hook script: `~/.claude/hooks/pr-review-gate.sh`
```bash
#!/bin/bash
# PreToolUse hook — blocks gh pr create until pre-PR review is done
set -uo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
# Only intercept gh pr create
echo "$COMMAND" | grep -qE '\bgh\s+pr\s+create\b' || exit 0
# State marker scoped to branch
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) || exit 0
BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null) || exit 0
STATE_FILE="/tmp/.pr-gate-${BRANCH}-pre-done"
# Check marker with TTL (1 hour)
if [ -f "$STATE_FILE" ]; then
AGE=$(( $(date +%s) - $(stat -c %Y "$STATE_FILE" 2>/dev/null || echo 0) ))
[ "$AGE" -le 3600 ] && exit 0 # valid marker — allow
rm -f "$STATE_FILE" # stale marker — re-gate
fi
# Block with instructions
cat <<'EOF'
{"decision":"block","reason":"Pre-PR review required. Run the /pre-pr-review skill first (type-check, tests, diff review, secret scan). The skill will create the gate marker when it passes. Then retry gh pr create."}
EOF
exit 0
```
### Post-hook script: `~/.claude/hooks/pr-post-checklist.sh`
```bash
#!/bin/bash
# PostToolUse hook — injects post-PR instructions after successful creation
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
echo "$COMMAND" | grep -qE '\bgh\s+pr\s+create\b' || exit 0
# Clean up gate marker
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) || exit 0
BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null) || true
rm -f "/tmp/.pr-gate-${BRANCH}-pre-done" 2>/dev/null
# Inject follow-up instructions (stdout shown to agent)
echo "PR created. Now: (1) verify all checklist items are checked, (2) wait for first CI results, (3) if OSS — check bot comments for CLA/triagebot warnings and fix immediately."
exit 0
```
### Settings entry
```json
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pr-review-gate.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pr-post-checklist.sh" }]
}
]
}
```
### What the agent experiences
```
Agent: Bash("gh pr create --title 'Fix widget' --body '...'")
→ ❌ BLOCKED: "Pre-PR review required. Run /pre-pr-review first..."
Agent: Skill("pre-pr-review") # runs typecheck, tests, diff review
→ ✅ All checks passed
→ touch /tmp/.pr-gate-fix-widget-pre-done
Agent: Bash("gh pr create --title 'Fix widget' --body '...'")
→ ✅ PR created: https://github.com/org/repo/pull/42
→ "PR created. Now: verify checklist, wait for CI, check bot comments..."
→ rm /tmp/.pr-gate-fix-widget-pre-done
```
---
## Example 2: RFC Lifecycle with CLI Command Surface (Advanced)
A multi-stage workflow where each stage has its own gate. Uses custom CLI commands as hookable surfaces.
**Problem**: RFC drafting has multiple stages (draft → review → revise → present) but agents jump straight to committing without iterative critic review. There's no natural command to intercept — agents just use `Write` and `git commit`.
**Solution**: Create `rfc` CLI commands that agents are told to use. Each command is a thin wrapper — the hooks enforce the real workflow.
```mermaid
flowchart TD
A["Agent told: use 'rfc draft slug'<br/>to start an RFC"] --> B{"PreToolUse Hook:<br/>worktree exists?"}
B -->|No| C["❌ BLOCK: Create a worktree first<br/>then retry rfc draft"]
B -->|Yes| D["✅ ALLOW: rfc draft runs<br/>(creates template file)"]
D --> E["Agent writes RFC content"]
E --> F["Agent told: use 'rfc review slug'<br/>to start review"]
F --> G{"PreToolUse Hook:<br/>RFC file exists?<br/>Has content?"}
G -->|No| H["❌ BLOCK: Write the RFC first<br/>then retry rfc review"]
G -->|Yes| I["✅ ALLOW: rfc review runs"]
I --> J["PostToolUse Hook injects:<br/>'Launch 3 parallel critic subagents.<br/>Incorporate feedback. Repeat 3 rounds.'"]
J --> K["Agent runs critic review rounds"]
K --> L["Agent told: use 'rfc submit slug'<br/>to finalize"]
L --> M{"PreToolUse Hook:<br/>review marker exists?<br/>(≥1 round completed)"}
M -->|No| N["❌ BLOCK: Run rfc review first"]
M -->|Yes| O["✅ ALLOW: rfc submit runs"]
O --> P["PostToolUse Hook injects:<br/>'Present RFC to user. Do NOT commit<br/>without explicit approval.'"]
style C fill:#fee,stroke:#c33
style H fill:#fee,stroke:#c33
style N fill:#fee,stroke:#c33
style D fill:#efe,stroke:#3c3
style I fill:#efe,stroke:#3c3
style O fill:#efe,stroke:#3c3
```
### CLI commands: `~/.local/bin/rfc`
```bash
#!/bin/bash
# Thin CLI surface — real enforcement is in hooks, real work is done by the agent
SLUG="$2"
RFC_DIR="docs/rfc"
case "$1" in
draft)
# Create template if it doesn't exist
FILE="$RFC_DIR/rfc-${SLUG}.md"
if [ ! -f "$FILE" ]; then
mkdir -p "$RFC_DIR"
cat > "$FILE" <<TEMPLATE
# RFC: ${SLUG}
**Status**: Draft
**Author**: $(git config user.name)
**Date**: $(date +%Y-%m-%d)
## Problem
## Proposed Solution
## Alternatives Considered
## Open Questions
TEMPLATE
echo "Created $FILE"
else
echo "RFC already exists: $FILE"
fi
;;
review)
echo "RFC review cycle starting for: rfc-${SLUG}.md"
# Marker: review was initiated (hook will inject critic instructions)
touch "/tmp/.rfc-${SLUG}-reviewed"
;;
submit)
echo "RFC ready for presentation: rfc-${SLUG}.md"
;;
*)
echo "Usage: rfc {draft|review|submit} <slug>"
exit 1
;;
esac
```
### Hook script: `~/.claude/hooks/rfc-gate.sh`
```bash
#!/bin/bash
# PreToolUse hook — enforces RFC workflow stages
set -uo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
# Only intercept rfc commands
echo "$COMMAND" | grep -qE '^\s*rfc\s+(draft|review|submit)\b' || exit 0
SUBCOMMAND=$(echo "$COMMAND" | grep -oP '(?<=rfc\s)(draft|review|submit)')
SLUG=$(echo "$COMMAND" | grep -oP '(?<='"$SUBCOMMAND"'\s)\S+')
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null) || exit 0
case "$SUBCOMMAND" in
draft)
# Gate: must be in a worktree (not main checkout)
if ! git -C "$CWD" rev-parse --is-inside-work-tree &>/dev/null; then
exit 0 # not in git — allow (edge case)
fi
TOPLEVEL=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null)
if [ -z "$(git -C "$CWD" worktree list --porcelain 2>/dev/null | grep -c 'worktree')" ]; then
echo '{"decision":"block","reason":"Create a git worktree before drafting an RFC. Run: git worktree add .claude/worktrees/rfc-'"$SLUG"' -b worktree-rfc-'"$SLUG"'. Then cd into it and retry."}'
exit 0
fi
;;
review)
# Gate: RFC file must exist and have content beyond template
RFC_FILE="$CWD/docs/rfc/rfc-${SLUG}.md"
if [ ! -f "$RFC_FILE" ]; then
echo '{"decision":"block","reason":"RFC file not found at docs/rfc/rfc-'"$SLUG"'.md. Run `rfc draft '"$SLUG"'` first to create it, then write the content."}'
exit 0
fi
LINES=$(wc -l < "$RFC_FILE")
if [ "$LINES" -lt 20 ]; then
echo '{"decision":"block","reason":"RFC is too short ('"$LINES"' lines). Write substantive content in all sections before starting review. Aim for at least 30 lines."}'
exit 0
fi
;;
submit)
# Gate: review must have been run at least once
if [ ! -f "/tmp/.rfc-${SLUG}-reviewed" ]; then
echo '{"decision":"block","reason":"RFC has not been reviewed. Run `rfc review '"$SLUG"'` first to trigger critic subagent review (3 rounds). Then retry submit."}'
exit 0
fi
;;
esac
exit 0 # allow
```
### Post-hook script: `~/.claude/hooks/rfc-post.sh`
```bash
#!/bin/bash
# PostToolUse hook — injects stage-specific follow-up instructions
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
echo "$COMMAND" | grep -qE '^\s*rfc\s+(draft|review|submit)\b' || exit 0
SUBCOMMAND=$(echo "$COMMAND" | grep -oP '(?<=rfc\s)(draft|review|submit)')
case "$SUBCOMMAND" in
draft)
echo "RFC template created. Now write substantive content in every section. When done, run \`rfc review <slug>\` to start the critic review cycle."
;;
review)
echo "Review cycle started. Launch 3 parallel critic subagents (boundary-critic, design-minimalist, delivery-pragmatist). Incorporate their feedback. Repeat for 3 rounds total. When satisfied, run \`rfc submit <slug>\`."
;;
submit)
echo "RFC is ready. Present the final RFC to the user and wait for explicit approval before committing or implementing. Do NOT auto-commit."
# Clean up all markers for this slug
SLUG=$(echo "$COMMAND" | grep -oP '(?<=submit\s)\S+')
rm -f "/tmp/.rfc-${SLUG}-reviewed" 2>/dev/null
;;
esac
exit 0
```
### What the agent experiences
```
Agent: Bash("rfc draft input-guard")
→ [if not in worktree] ❌ BLOCK: "Create a worktree first..."
→ [in worktree] ✅ "Created docs/rfc/rfc-input-guard.md"
→ PostToolUse: "Now write substantive content. When done, run rfc review..."
(Agent writes the RFC)
Agent: Bash("rfc review input-guard")
→ [if RFC < 20 lines] ❌ BLOCK: "RFC too short, write more content first"
→ [if RFC ready] ✅ "Review cycle starting"
→ PostToolUse: "Launch 3 parallel critics. 3 rounds. Then rfc submit..."
(Agent runs 3 critic rounds)
Agent: Bash("rfc submit input-guard")
→ [if no review marker] ❌ BLOCK: "Run rfc review first"
→ [if reviewed] ✅ "RFC ready for presentation"
→ PostToolUse: "Present to user. Do NOT commit without approval."
→ Cleanup: rm /tmp/.rfc-input-guard-reviewed
```
---
## Pattern 2: CLI Command Surfaces
Sometimes you want to enforce workflows around actions that don't naturally map to a hookable tool call. The solution: **create a lightweight CLI command whose sole purpose is to give hooks a surface to intercept.**
### The idea
Instead of telling agents "use `gh pr create`" (which is a real command), tell them "use `rfc write <title>`" (which is your custom command). The command itself can be trivial — even a no-op — but it gives you:
1. A **PreToolUse hook point** to enforce prerequisites
2. A **PostToolUse hook point** to inject follow-up instructions
3. A **named workflow step** that's easy to match in hook config
4. An **audit trail** for free (every Bash tool call is logged)
### More command surface ideas
| Workflow | Custom command | PreToolUse enforces | PostToolUse instructs |
|----------|---------------|--------------------|-----------------------|
| RFC lifecycle | `rfc draft/review/submit` | Worktree, content, review round | Next stage instructions |
| OSS contribution | `oss submit <repo>` | Run 4 reviewer specialists | Check bot comments, fix warnings |
| Deploy | `deploy prod` | Run full test suite | Monitor health endpoint for 5 min |
| Feature start | `feature start <name>` | Create issue + worktree | Run TDD workflow skill |
| Release | `release cut <version>` | Changelog updated, tests green | Tag, push, create GitHub release |
### Why this works
Agents follow natural language instructions to "use command X" very reliably — it's a concrete action, not a vague guideline. By creating a named command, you:
1. Make the workflow step **explicit** in the agent's task prompt
2. Give hooks a **reliable interception point**
3. Create a **vocabulary** for workflows that agents and hooks share
---
## When to Use Each Approach
| Signal | Use hooks | Use CLAUDE.md/skills |
|--------|-----------|---------------------|
| Agent has skipped this step multiple times | Yes | No (already failed) |
| Step is a hard invariant (must never be skipped) | Yes | Also document why |
| Step is a soft guideline (usually but not always) | No | Yes |
| You need an audit trail | Yes (tool calls are logged) | No |
| The step applies to all projects | Yes (user settings.json) | Yes (~/.claude/CLAUDE.md) |
| The step is project-specific | Yes (project settings.json) | Yes (project CLAUDE.md) |
| You want to enforce ordering (A before B) | Yes (block B until A marks done) | Fragile |
## Composability
Multiple hooks on the same event run in sequence. If **any** hook blocks, the tool call is blocked. This means:
- Rate limiting hook (`oss-contribution-gate`) + pre-PR review hook can coexist on `gh pr create`
- Each hook checks its own concern independently
- Order doesn't matter for blocking decisions (any block wins)
- For PostToolUse, all hooks run and all feedback is concatenated
## Anti-Patterns
- **Don't use hooks for advice.** If the agent can reasonably skip the step in some cases, use CLAUDE.md. Hooks are for invariants.
- **Don't create commands you never hook.** CLI command surfaces only make sense if you wire them to hooks. Otherwise, just use CLAUDE.md instructions.
- **Don't over-gate.** Every blocked command costs one agent retry cycle. Gate the critical moments (PR creation, deployment), not every minor step.
- **Don't forget state cleanup.** Stale state markers from crashed sessions will block future commands. Always include TTL or cleanup logic.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!