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.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Pattern for implementing slash commands that execute entirely in the hook handler, bypassing the Claude API call entirely.
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
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.
hooks.json): Routes UserPromptSubmit to handler binaryinternal/hooks/prompt_handler.go): Regex matching + dispatchcommands/*.md): MUST exist for /help discovery (body ignored)In prompt_handler.go:
var newCmdPattern = regexp.MustCompile(`^/(?:claude-bumper-lanes:)?bumper-foo\s*(.*)$`)
The (?:claude-bumper-lanes:)? makes the plugin namespace optional.
In HandlePrompt():
if m := newCmdPattern.FindStringSubmatch(prompt); m != nil {
return handleFoo(sessionID, strings.TrimSpace(m[1]))
}
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
}
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.
just build-bumper-lanes
Two helpers reduce boilerplate:
func loadSessionOrBlock(sessionID string) *state.SessionState
Returns session state or nil. If nil, error already shown to user via blockPrompt().
func saveOrBlock(sess *state.SessionState) bool
Returns true on success. If false, error already shown to user via blockPrompt().
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))
}
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 |
blockPrompt() is called and JSON printed to stdoutcommands/*.md stub file existsjust build-bumper-lanes after changes