hook-intercept-block

kylesnowschwartz's avatarfrom kylesnowschwartz

This skill should be used when implementing slash commands that execute without Claude API calls. Use when: adding a new /bumper-* command, understanding why commands return "block" responses, debugging UserPromptSubmit hooks, or learning the pattern for instant command execution. Keywords: UserPromptSubmit, block decision, hook response, slash command implementation.

⭐15stars🔀1forks📁View on GitHub🕐Updated Jan 2, 2026

When & Why to Use This Skill

This Claude skill provides a technical framework for the 'Hook-Intercept-Block' pattern, allowing developers to implement slash commands that execute locally without calling the Claude API. By utilizing the UserPromptSubmit hook, it enables high-speed, deterministic command execution, reduces API costs, and provides a standardized method for extending Claude Code's functionality through Go-based logic.

Use Cases

  • Developing custom slash commands (e.g., /reset, /config) that perform local system operations or state management without LLM latency.
  • Optimizing agent performance by intercepting specific regex-matched prompts to return instant, non-probabilistic responses.
  • Debugging and troubleshooting UserPromptSubmit hooks to understand why commands return 'block' responses or fail to trigger.
  • Implementing cost-effective command stubs that appear in /help discovery but handle logic entirely within the local environment.
namehook-intercept-block
description>
Claude API calls. Use whenadding a new /bumper-* command, understanding why
the pattern for instant command execution. KeywordsUserPromptSubmit, block decision,

Hook-Intercept-Block Pattern

Pattern for implementing slash commands that execute entirely in the hook handler, bypassing the Claude API call entirely.

Why Use This Pattern

  • No API cost: Commands execute in Go, no Claude API call
  • Faster: Direct execution vs markdown parsing + API round trip
  • Deterministic: No model variance - same input, same output

How It Works

User types: /bumper-reset
    ↓
UserPromptSubmit hook fires
    ↓
prompt_handler.go matches regex: ^/(?:claude-bumper-lanes:)?bumper-reset\s*$
    ↓
handleReset() executes Go logic
    ↓
Returns JSON to stdout: {"decision":"block","reason":"Baseline reset. Score: 0/400"}
    ↓
Claude Code shows "reason" to user, skips API call

The Confusing Naming

Claude Code's hook response API uses counterintuitive terminology:

Response What It Actually Means
decision: "block" "I handled this, don't call Claude API" (NOT "blocked/rejected")
decision: "continue" "Let it through to Claude API"
reason: "..." Message shown to user (only with "block")

Key insight: block = "handled and done", not "rejected". The command succeeded.

Implementation Components

  1. Hook config (hooks.json): Routes UserPromptSubmit to handler binary
  2. Handler (internal/hooks/prompt_handler.go): Regex matching + dispatch
  3. Command stubs (commands/*.md): MUST exist for /help discovery (body ignored)

Adding a New Command

Step 1: Add Regex Pattern

In prompt_handler.go:

var newCmdPattern = regexp.MustCompile(`^/(?:claude-bumper-lanes:)?bumper-foo\s*(.*)$`)

The (?:claude-bumper-lanes:)? makes the plugin namespace optional.

Step 2: Add Dispatch

In HandlePrompt():

if m := newCmdPattern.FindStringSubmatch(prompt); m != nil {
    return handleFoo(sessionID, strings.TrimSpace(m[1]))
}

Step 3: Implement Handler

Use the helper functions for DRY session management:

func handleFoo(sessionID, args string) int {
    sess := loadSessionOrBlock(sessionID)
    if sess == nil {
        return 0
    }

    // ... your logic here ...

    if !saveOrBlock(sess) {
        return 0
    }

    blockPrompt("Success message")
    return 0
}

Step 4: Create Command Stub

Create commands/bumper-foo.md:

---
description: Does the foo thing
argument-hint: <optional-args>
---

This command is handled by the hook system.

The markdown body is ignored - the hook handles everything. The file MUST exist for the command to appear in /help.

Step 5: Rebuild

just build-bumper-lanes

Helper Functions

Two helpers reduce boilerplate:

loadSessionOrBlock

func loadSessionOrBlock(sessionID string) *state.SessionState

Returns session state or nil. If nil, error already shown to user via blockPrompt().

saveOrBlock

func saveOrBlock(sess *state.SessionState) bool

Returns true on success. If false, error already shown to user via blockPrompt().

JSON Response Format

The UserPromptResponse struct:

type UserPromptResponse struct {
    Decision string `json:"decision,omitempty"`
    Reason   string `json:"reason,omitempty"`
}

Output via blockPrompt():

func blockPrompt(reason string) {
    resp := UserPromptResponse{
        Decision: "block",
        Reason:   reason,
    }
    out, _ := json.Marshal(resp)
    fmt.Println(string(out))
}

Existing Commands Using This Pattern

All bumper-lanes slash commands use hook-intercept-block:

Command Handler Purpose
/bumper-reset handleReset() Capture new baseline, reset score
/bumper-pause handlePause() Disable enforcement
/bumper-resume handleResume() Re-enable enforcement
/bumper-view handleView() Set/show visualization mode
/bumper-config handleConfig() Show/set threshold

Debugging Tips

  1. Command not recognized: Check regex pattern matches user input exactly
  2. No output shown: Ensure blockPrompt() is called and JSON printed to stdout
  3. Command not in /help: Verify commands/*.md stub file exists
  4. Binary not updated: Run just build-bumper-lanes after changes