Forge-core BuildHook
Create and validate module hooks. USE WHEN create hook, new hook, write hook, scaffold hook, validate hook, check hook, hook conventions, hook events, hook structure.
git clone https://github.com/N4M3Z/forge-core
T=$(mktemp -d) && git clone --depth=1 https://github.com/N4M3Z/forge-core "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/BuildHook" ~/.claude/skills/n4m3z-forge-core-buildhook && rm -rf "$T"
skills/BuildHook/SKILL.mdBuildHook
Create and validate hook scripts for forge modules. Hooks are bash scripts triggered by Claude Code events, routed through the
dispatch binary.
Workflow Routing
| Workflow | Trigger | Section |
|---|---|---|
| Create | "create hook", "new hook", "scaffold hook" | Create Workflow |
| Validate | "validate hook", "check hook" | Validate Workflow |
Hook Conventions
Events and Output Modes
Every hook handles one of 9 Claude Code events. Each event has a fixed output mode that determines how module output is handled:
| Event | Mode | Behaviour |
|---|---|---|
| Concatenate | All module outputs combined and emitted to AI context |
| Concatenate | All module outputs combined and emitted to AI context |
| Gate | Exit 2 blocks the tool call; exit 0 allows |
| Gate | Exit 2 blocks session exit; exit 0 allows |
| Gate | Exit 2 blocks subagent exit; exit 0 allows |
| Passive | Output discarded; runs for side effects only |
| Passive | Output discarded; runs for side effects only |
| Passive | Output discarded; runs for side effects only |
| Passive | Output discarded; runs for side effects only |
Output Mode Decision Table
Use this when choosing which event to hook:
| Goal | Event | Mode | Notes |
|---|---|---|---|
| Inject context at session start | | Concatenate | Emit markdown to stdout |
| Block a tool call (access control) | | Gate | Exit 2 to block, 0 to allow |
| Enforce rules before exit | | Gate | Exit 2 to block, 0 to allow |
| React to a tool result | | Passive | Side effects only, output discarded |
| Clean up after session | | Passive | Side effects only, output discarded |
| Inject context before compaction | | Passive | Emit markdown to stdout |
File Naming
Hook scripts use PascalCase matching the event name:
| Event | Filename |
|---|---|
| |
| |
| |
| |
| |
Dual-Mode Preamble
Every hook script starts with this template. It resolves the module root from either forge-core dispatch or standalone plugin context:
#!/usr/bin/env bash set -euo pipefail MODULE_ROOT="${FORGE_MODULE_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(command cd "$(dirname "$0")/.." && pwd)}}"
Stdin JSON
Claude Code pipes a JSON payload to hook scripts on stdin. The schema varies by event:
- PreToolUse / PostToolUse:
{"tool_name":"...", "tool_input":{...}} - Stop:
{"stop_reason":"...", ...} - SessionStart: empty or
{}
Read stdin once:
INPUT=$(cat). Parse with yq -p json or a compiled binary.
Registration Chain
For dispatch to find a hook:
- Hook file exists at
and is executablehooks/<EventName>.sh
lists the event inmodule.yaml
(Tier 1 check)events:- Module is listed in project
underdefaults.yaml
(Tier 0)modules:
The 3-tier event check:
config.yaml override (authoritative) > module.yaml events > hook file existence (fallback).
Exit Code Conventions
| Mode | Exit 0 | Exit 2 | Other |
|---|---|---|---|
| Gate | Allow | Block | Treated as allow |
| Concatenate | Success | N/A | Output included regardless |
| Passive | Success | N/A | Output discarded regardless |
Gate hooks that cannot build or run should exit 0 (graceful degradation — never block Claude on infrastructure failure).
Create Workflow
Step 1: Determine event and output mode
Ask the user:
- What should trigger this hook? (Use the Output Mode Decision Table above)
- What should the hook do? (Inject context, block an action, or run a side effect?)
Step 2: Scaffold the hook script
Create
hooks/<EventName>.sh with:
#!/usr/bin/env bash # <EventName> hook: <brief description>. set -euo pipefail MODULE_ROOT="${FORGE_MODULE_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(command cd "$(dirname "$0")/.." && pwd)}}" INPUT=$(cat) # Gate: exit 2 to block, 0 to allow | Concatenate: emit context to stdout
For Gate hooks, add exit code logic. For Passive hooks, add the side effect. For Concatenate hooks, emit context to stdout.
Make the script executable:
chmod +x hooks/<EventName>.sh
Step 3: Register the event
Add the event to
module.yaml:
events: - <EventName>
Step 4: Add standalone hooks.json entry (optional)
If the module also works as a standalone Claude Code plugin, add the event to
hooks/hooks.json:
{ "hooks": { "<EventName>": [ {"hooks": [{"type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/<EventName>.sh"}]} ] } }
Step 5: Add hook config to defaults.yaml (optional)
If the hook needs configurable settings:
hooks: HookName: key: value
Read with
yq '.hooks.HookName.key' "$MODULE_ROOT/defaults.yaml". Override via config.yaml.
Step 6: Verify
Run the hook manually to test:
echo '{"tool_name":"TestTool"}' | bash hooks/<EventName>.sh
Validate Workflow
Step 1: Read the target hook
Read the hook script and
module.yaml.
Step 2: Check structure
- File exists at
hooks/<EventName>.sh - File is executable (
)chmod +x - Starts with
#!/usr/bin/env bash - Has
set -euo pipefail - Dual-mode MODULE_ROOT resolution present
- Uses
prefix forcommand
,cd
,cp
,mvrm
Step 3: Check registration
- Event listed in
module.yaml
arrayevents: - Module listed in project
defaults.yaml
arraymodules: - If standalone:
references correct filenamehooks/hooks.json
Step 4: Check output mode compliance
- Gate hooks: uses exit 2 to block, exit 0 to allow
- Gate hooks: exits 0 on build/infrastructure failure (graceful degradation)
- Concatenate hooks: emits useful markdown to stdout
- Passive hooks: does not depend on stdout being visible
Step 5: Report
COMPLIANT -- all checks pass.
NON-COMPLIANT -- list failures with specific fixes. Offer to fix automatically.
Constraints
- Hook filenames MUST use PascalCase matching the event name:
, notSessionStart.shsession-start.sh - Shell scripts MUST use
andset -euo pipefail
prefix for aliased commandscommand - Gate hooks MUST exit 0 on infrastructure failure — never block Claude due to a broken hook
- Stdin is consumed once — read it into a variable (
) before processingINPUT=$(cat) - Output is mode-dependent — Passive hooks cannot communicate back to the AI