GigiKit Guides

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

  1. Choose the lifecycle event: pre-tool-use, post-tool-use, subagent-start, or stop
  2. Create a script in the corresponding directory under .claude/hooks/
  3. Make it executable: chmod +x .claude/hooks/pre-tool-use/my-hook.sh
  4. 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:

  1. Parse the JSON between @@PRIVACY_PROMPT_START@@ and @@PRIVACY_PROMPT_END@@
  2. Call AskUserQuestion with the parsed question data
  3. 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

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 jq for JSON parsing, not string manipulation
  • Returns exit 0 for all tool calls it does not intend to affect
  • Stdout on exit 1 is 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