Building Custom Hooks
Custom hooks let you extend GigiKit’s behavior at any lifecycle point without touching agent prompts. This guide walks through creating a hook from scratch and covers the privacy block pattern built into GigiKit.
Creating a Hook Script
- Choose the lifecycle event:
pre-tool-use,post-tool-use,subagent-start, orstop - Create a script in the corresponding directory under
.claude/hooks/ - Make it executable:
chmod +x .claude/hooks/pre-tool-use/my-hook.sh - The runtime will automatically pick it up — no registration required
#!/usr/bin/env bash
# my-hook.sh — example PreToolUse hook
# Reads JSON from stdin, decides whether to block the tool call
INPUT=$(cat)
# Extract the tool name using jq
TOOL=$(echo "$INPUT" | jq -r '.tool')
# Only act on Write calls
if [ "$TOOL" != "Write" ]; then
exit 0 # allow all other tools
fi
# Extract the target file path
FILE_PATH=$(echo "$INPUT" | jq -r '.params.file_path')
# Block writes to the plans/reports directory from non-reporter agents
if [[ "$FILE_PATH" == */plans/reports/* ]]; then
echo "Hook blocked: only reporter agents may write to plans/reports/"
exit 1
fi
exit 0 # allow
The Privacy Block Pattern
GigiKit ships a privacy-block hook that intercepts reads of sensitive files (.env, *.key, credential files). When triggered it does not silently block — it injects a structured JSON prompt so the agent can ask the user for explicit approval.
How the hook works
#!/usr/bin/env bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool')
FILE=$(echo "$INPUT" | jq -r '.params.file_path // ""')
# Patterns that require user approval
SENSITIVE_PATTERNS=(".env" "*.key" "*credentials*" "*secret*")
for PATTERN in "${SENSITIVE_PATTERNS[@]}"; do
if [[ "$FILE" == $PATTERN ]]; then
# Emit structured JSON wrapped in markers — agent MUST use AskUserQuestion
echo "@@PRIVACY_PROMPT@@"
cat <<JSON
@@PRIVACY_PROMPT_START@@
{
"tool": "AskUserQuestion",
"questions": [{
"question": "I need to read \"$(basename $FILE)\" which may contain sensitive data. Do you approve?",
"header": "File Access",
"options": [
{ "label": "Yes, approve access", "description": "Allow reading this time" },
{ "label": "No, skip this file", "description": "Continue without accessing this file" }
],
"multiSelect": false
}]
}
@@PRIVACY_PROMPT_END@@
JSON
exit 2 # block and inject context
fi
done
exit 0
How the agent must respond
When an agent receives the @@PRIVACY_PROMPT@@ marker in a hook output, it must follow this exact flow — no workarounds:
- Parse the JSON between
@@PRIVACY_PROMPT_START@@and@@PRIVACY_PROMPT_END@@ - Call
AskUserQuestionwith the parsed question data - Based on the user’s selection:
- “Yes, approve access” — use
bash cat "filepath"to read the file (bash is auto-approved and bypasses the hook) - “No, skip this file” — continue the task without accessing the file
- “Yes, approve access” — use
Injecting Context via Hooks (Exit Code 2)
Use exit code 2 when you want to add information to the agent’s context without blocking the tool call. The hook’s stdout is appended to the agent’s next message as additional context.
#!/usr/bin/env bash
# Injects active plan directory and reports path into every new subagent session
INPUT=$(cat)
PLAN_DIR=$(echo "$INPUT" | jq -r '.env.ACTIVE_PLAN_DIR // ""')
if [ -n "$PLAN_DIR" ]; then
cat <<CONTEXT
## Plan Context
Active plan: $PLAN_DIR
Reports: $PLAN_DIR/reports/
Visuals: $PLAN_DIR/visuals/
CONTEXT
exit 2 # inject without blocking
fi
exit 0
Hook Checklist
Before deploying a custom hook:
- Script is executable (
chmod +x) - Reads stdin with
INPUT=$(cat)before any processing - Uses
jqfor JSON parsing, not string manipulation - Returns
exit 0for all tool calls it does not intend to affect - Stdout on
exit 1is a clear, actionable error message for the agent - Tested locally by piping a sample JSON payload:
echo '{"tool":"Write","params":{"file_path":"/tmp/test"}}' | ./my-hook.sh
Related Guides
- Hook System Overview — lifecycle events, directory structure, input/output format
- Development Rules: YAGNI, KISS, DRY — the rules hooks help enforce automatically