agenthooks

package module
v1.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

Checkmarx Agent Hooks

CI Go Reference Go Report Card Go Version License Release

One hook codebase for every AI coding agent. Write a single handler, compile one binary, and it runs across Claude Code · Cursor · Windsurf Cascade · Factory Droid · Gemini CLI · GitHub Copilot (VS Code) · GitHub Copilot CLI — no per-platform code.

package main

import (
    "strings"

    "github.com/Checkmarx/ast-cx-hooks"
)

func main() {
    agenthooks.BeforeToolCall(func(e agenthooks.ToolCallEvent) agenthooks.ToolVerdict {
        if e.IsShell() && strings.Contains(e.Command, "rm -rf") {
            return agenthooks.Deny("Destructive commands are not allowed.")
        }
        return agenthooks.Allow()
    })
    agenthooks.Dispatch()
}

That one handler gates shell commands in all seven agents — the library translates the wire format (JSON schema, response shape, blocking semantics) per platform.

Installation

go get github.com/Checkmarx/ast-cx-hooks

Zero dependencies (standard library only).

Supported agents

Agent Config location Output style
Claude Code ~/.claude/settings.json nested JSON decision
Cursor ~/.cursor/hooks.json flat JSON decision
Windsurf Cascade ~/.codeium/windsurf/hooks.json exit code 2 only
Factory Droid ~/.factory/settings.json nested JSON / exit 2
Gemini CLI ~/.gemini/settings.json JSON decision / exit 2
GitHub Copilot (VS Code) .github/hooks/*.json (project-scoped) nested JSON decision
GitHub Copilot CLI ~/.copilot/hooks/agenthooks.json flat JSON / exit 2

The VS Code Copilot extension and the Copilot CLI are different products with incompatible hook schemas (nested vs. flat output, updatedInput vs. modifiedArgs, runTerminalCommand vs. bash), so they are separate packages — copilot and copilotcli.

Unified hooks

8 unified hook categories map automatically to each platform's native events.

WhenAgentIdle — the agent finished responding
agenthooks.WhenAgentIdle(func(e agenthooks.AgentIdleEvent) agenthooks.IdleVerdict {
    if e.IsLooping() {
        return agenthooks.Resume() // break infinite continuation loops
    }
    return agenthooks.Interrupt("Please run the tests before finishing.")
})

Verdicts: Resume() · Interrupt(feedback)

BeforeToolCall — gate tool & command execution
agenthooks.BeforeToolCall(func(e agenthooks.ToolCallEvent) agenthooks.ToolVerdict {
    if e.IsShell() {
        return agenthooks.AskUser("Please confirm this shell command.")
    }
    return agenthooks.AllowWithInput(sanitize(e.ToolArgs)) // rewrite tool input before it runs
})

Verdicts: Allow() · AllowWithNote(msg) · AllowWithContext(ctx) · AllowWithInput(json) · AskUser(reason) · Deny(reason) · DenyWithContext(reason, ctx)

BeforeFileEdit — gate a file write before it lands (can block)

Fires before the agent writes/edits a file, with the proposed Changes available so a handler can scan the content and deny the write — the route a security scan uses to block a vulnerable change before it reaches disk. (Contrast with AfterFileWrite, which fires post-write and can only annotate.)

agenthooks.BeforeFileEdit(func(e agenthooks.FileEditEvent) agenthooks.FileEditVerdict {
    for _, d := range e.Changes {
        if scanForSecret(d.After) {
            return agenthooks.RejectEditWithContext(
                "Secret detected in "+e.FilePath,
                "Remove the secret and retry the write.") // additionalContext steers the agent
        }
    }
    return agenthooks.AcceptEdit()
})

Verdicts: AcceptEdit() · AcceptEditWithInput(json) · RejectEdit(reason) · RejectEditWithContext(reason, ctx) · AskBeforeEdit(reason)

AfterFileWrite — react to a completed file edit
agenthooks.AfterFileWrite(func(e agenthooks.FileWriteEvent) agenthooks.FileWriteVerdict {
    if strings.HasSuffix(e.FilePath, ".go") {
        return agenthooks.AnnotateWrite("Reminder: run `go vet` before committing.")
    }
    return agenthooks.AcceptWrite()
})

Verdicts: AcceptWrite() · RejectWrite(reason) · RejectWriteWithContext(reason, ctx) · AnnotateWrite(note)

BeforePrompt — filter or enrich user prompts
agenthooks.BeforePrompt(func(e agenthooks.PromptEvent) agenthooks.PromptVerdict {
    if containsSecret(e.Text) {
        return agenthooks.RejectPrompt("Prompt contains sensitive information.")
    }
    return agenthooks.EnrichPrompt("Always follow our coding standards.")
})

Verdicts: AcceptPrompt() · RejectPrompt(msg) · EnrichPrompt(context)

WhenSubagentIdle — gate subagent completion

Reuses AgentIdleEvent / IdleVerdict; Interrupt blocks the subagent from stopping. Verdicts: Resume() · Interrupt(feedback)

AfterToolFailure — react to failed tool calls
agenthooks.AfterToolFailure(func(e agenthooks.ToolFailureEvent) agenthooks.ToolFailureVerdict {
    return agenthooks.AnnotateFailure("Tool " + e.ToolName + " failed: " + e.Error)
})

Verdicts: AcknowledgeFailure() · AnnotateFailure(note) · RejectAfterFailure(reason)

BeforeFileRead — gate the agent reading a file
agenthooks.BeforeFileRead(func(e agenthooks.FileReadEvent) agenthooks.FileReadVerdict {
    if strings.HasSuffix(e.FilePath, ".env") {
        return agenthooks.DenyRead("Reading secret files is not allowed.")
    }
    return agenthooks.AllowRead()
})

Verdicts: AllowRead() · DenyRead(reason)

Support matrix

Which unified hooks each agent supports today:

Unified hook Claude Cursor Windsurf Droid Gemini Copilot Copilot CLI
WhenAgentIdle ✅ ¹
BeforeToolCall
BeforeFileEdit ✅ ⁴ ✅ ⁴
AfterFileWrite ✅ ¹
BeforePrompt ✅ ³
WhenSubagentIdle
AfterToolFailure ✅ ²
BeforeFileRead

¹ fire-and-forget — feedback is logged, not enforced by the agent. ² observational on Cursor — the verdict is ignored. ³ observational on Copilot CLI — userPromptSubmitted output is not processed. ⁴ additionalContext on a deny is delivered only where the agent's hook protocol defines that field — Claude and VS Code Copilot on the pre-tool / pre-file gates; elsewhere a deny carries its reason only (context is dropped and logged).

Multiple implementations per hook (scenarios)

Several teams — or scenarios — can share one hook. Register the default with BeforeToolCall(fn) and any number of named scenarios with BeforeToolCallScenario(name, fn). The caller passes a scenario key; the framework runs the matching handler (or the default).

agenthooks.BeforeToolCallScenario("phoenix", phoenix.Handle)
agenthooks.BeforeToolCallScenario("cypher",  cypher.Handle)
agenthooks.BeforeToolCall(func(e agenthooks.ToolCallEvent) agenthooks.ToolVerdict {
    return agenthooks.Allow() // default when no scenario key matches
})

The key is supplied per invocation (default selector = ScenarioFromArg):

Channel How
CLI arg myhook claude-pre-tool-use phoenix
Env var AGENTHOOKS_SCENARIO=phoenix myhook claude-pre-tool-use
Event content agenthooks.UseScenarioSelector(func(m agenthooks.ScenarioMeta) string { … })

Resolution order: a matching named scenario → the registered default → a fallback verdict (fail-closed for the gating hooks). Every unified hook has a …Scenario variant.

CLI: scaffold, build, install

# Scaffold a starter hooks project (main.go + policy.json + .gitignore)
go run github.com/Checkmarx/ast-cx-hooks/cmd/agenthooks init --dir ./my-hooks

# Build for the current platform, or cross-compile to dist/ (macOS/Linux/Windows × amd64/arm64)
go build -o myhook .
go run github.com/Checkmarx/ast-cx-hooks/cmd/agenthooks build

# Write the correct hook config — in each agent's own shape — into every settings file
go run github.com/Checkmarx/ast-cx-hooks/cmd/agenthooks install ./myhook

install writes to ~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codeium/windsurf/hooks.json, ~/.factory/settings.json, ~/.gemini/settings.json, and ~/.copilot/hooks/agenthooks.json. The VS Code Copilot extension is project-scoped (.github/hooks/*.json) and is set up by hand.

Embedding installation. Consumers that wire these hooks into their own CLI can import the install package directly — install.InstallClaude/InstallCursor/InstallWindsurf/InstallDroid/InstallGemini/InstallCopilotCLI(home, cmdFor) plus install.FormatCommand and the install.CmdForFunc type — to control the command each route maps to (e.g. cx hooks <route>). Route → settings-file / event-key / encoding is driven by the single agenthooks.Catalog source of truth.

Platform-specific hooks

Need raw, per-platform event data? Use AddRoute with a platform package:

import (
    "github.com/Checkmarx/ast-cx-hooks"
    "github.com/Checkmarx/ast-cx-hooks/claude"
)

agenthooks.AddRoute("claude-pre-tool-use", func() {
    agenthooks.Process(func(e claude.PreToolUseEvent) claude.PreToolUseResult {
        if e.ToolName == "Bash" {
            return claude.DenyToolUse("Shell commands are disabled.")
        }
        return claude.ApproveToolUse()
    })
})

Packages: claude · cursor · windsurf · droid · gemini · copilot · copilotcli. Each models its agent's full event surface and ships response builders (additionalContext, tool-input/output rewrite, permission decisions, and more).

Testing

Hook binaries read JSON from stdin and write JSON to stdout; the first arg is the route:

go build -o myhook .

# Claude pre-file-write (block a vulnerable write before it lands)
echo '{"tool_name":"Write","tool_input":{"file_path":"/app/x.py","content":"eval(user)"},"session_id":"t"}' | ./myhook claude-pre-file-write

# Cursor stop
echo '{"status":"completed","loop_count":0,"conversation_id":"t"}' | ./myhook cursor-stop

# Copilot CLI pre-tool-use (lowercase tool names + FLAT output)
echo '{"hook_event_name":"PreToolUse","tool_name":"bash","tool_input":{"command":"rm -rf /"}}' | ./myhook copilot-cli-pre-tool-use

Unit-test a handler directly — it's plain Go:

func TestDenyDangerousCommands(t *testing.T) {
    e := agenthooks.ToolCallEvent{Kind: agenthooks.ToolKindShell, Command: "rm -rf /important"}
    if myToolCallHandler(e).Permit {
        t.Fatal("expected dangerous command to be denied")
    }
}

Architecture

github.com/Checkmarx/ast-cx-hooks
├── agenthooks.go        # Core: AddRoute, Dispatch, Process/ProcessE, RouteNames
├── unified.go           # The 8 unified hooks → thin registry over platform adapters
├── registry.go          # Generic registerAdapters wiring
├── route_catalog.go     # Single source of truth: route → settings file / event key / style
├── scenarios.go         # Per-hook registries + named-scenario dispatch (fail-closed gates)
├── aliases.go           # Re-exports the hookcore vocabulary as the public API
├── install/             # Importable installer (InstallClaude/…, CmdForFunc) — Catalog-driven
├── internal/hookcore/   # Leaf vocabulary: events, verdicts, Run/RunE, ToolConvention (tool naming)
├── claude|…|copilotcli/ # Per-platform types, response builders, and adapters.go (translation)
├── internal/scaffold/   # Templates + generator for `agenthooks init`
└── cmd/agenthooks/      # CLI: init · build · install

Flow: the agent invokes myhook <route>Dispatch looks up the route → the platform's adapters.go decodes the event, builds a unified event, calls your handler, and maps the verdict back to that platform's response — all via hookcore.Run/RunE. The unified vocabulary lives in the leaf internal/hookcore package so every platform package shares it without an import cycle, and the public API stays agenthooks.*.

API reference

Core: AddRoute(name, fn) · Dispatch() · Process(handler) · ProcessE(handler) (returned error blocks via exit 2) · RouteNames()

Unified hooks: WhenAgentIdle · WhenSubagentIdle · BeforeToolCall · BeforeFileEdit · AfterFileWrite · AfterToolFailure · BeforeFileRead · BeforePrompt (each with a …Scenario variant)

Event helpers: AgentIdleEvent.IsLooping() · ToolCallEvent.IsShell() · ToolCallEvent.IsMCP()

Security model

These hooks gate agent actions, so failure behavior matters:

  • Malformed / unparseable stdin → fail OPEN. A hook that cannot decode its input logs to stderr and exits 0 with no decision, so a bad payload never blocks the agent. If you rely on a hook as a hard control, monitor its stderr — a wire-format change could otherwise silently disable the gate.
  • Misconfigured scenarios → fail CLOSED. A gating hook (BeforeToolCall, BeforeFileEdit, BeforeFileRead, BeforePrompt) with named scenarios but no default and no match denies rather than allowing, and logs why.
  • install is non-destructive. It merges into existing settings (preserving your other hooks), aborts rather than overwriting a file it cannot parse, and writes a .bak first.

Documentation

Contributing

See CONTRIBUTING.md for development setup, module architecture, and contribution guidelines. Please also read our Code of Conduct. Maintainers are listed in MAINTAINERS.md and code ownership in CODEOWNERS.txt.

Security

To report a vulnerability, follow the process in SECURITY.md — please do not open a public issue for security reports. For the library's runtime failure behavior, see the Security model above.

License

Apache 2.0 — see LICENSE.txt for details.


Website: Checkmarx.

© 2026 Checkmarx Ltd. All Rights Reserved.

Documentation

Overview

Package agenthooks provides a framework for building hooks for AI coding agents.

It supports Claude Code, Cursor, Windsurf Cascade, Factory Droid, Gemini CLI, and VS Code Copilot (Preview) through a single unified API plus platform-specific packages for advanced use. (Copilot hooks are project-scoped, so `agenthooks install` configures the other five; see the README for Copilot setup.)

Quick start with unified handlers (one handler works across all agents):

package main

import "github.com/Checkmarx/ast-cx-hooks"

func main() {
    agenthooks.WhenAgentIdle(func(e agenthooks.AgentIdleEvent) agenthooks.IdleVerdict {
        if e.IsLooping() {
            return agenthooks.Resume()
        }
        return agenthooks.Interrupt("Please review changes before finishing.")
    })
    agenthooks.Dispatch()
}

Platform-specific handlers for advanced use:

agenthooks.AddRoute("claude-pre-tool-use", func() {
    agenthooks.Process(func(e claude.PreToolUseEvent) claude.PreToolUseResult {
        return claude.ApproveToolUse()
    })
})

Index

Constants

View Source
const (
	AgentClaude     = hookcore.AgentClaude
	AgentCursor     = hookcore.AgentCursor
	AgentWindsurf   = hookcore.AgentWindsurf
	AgentDroid      = hookcore.AgentDroid
	AgentGemini     = hookcore.AgentGemini
	AgentCopilot    = hookcore.AgentCopilot
	AgentCopilotCLI = hookcore.AgentCopilotCLI

	ToolKindShell   = hookcore.ToolKindShell
	ToolKindMCP     = hookcore.ToolKindMCP
	ToolKindBuiltin = hookcore.ToolKindBuiltin
)

Variables

View Source
var (
	Resume    = hookcore.Resume
	Interrupt = hookcore.Interrupt

	Allow            = hookcore.Allow
	AllowWithNote    = hookcore.AllowWithNote
	Deny             = hookcore.Deny
	AskUser          = hookcore.AskUser
	AllowWithInput   = hookcore.AllowWithInput
	AllowWithContext = hookcore.AllowWithContext
	DenyWithContext  = hookcore.DenyWithContext

	AcceptWrite            = hookcore.AcceptWrite
	RejectWrite            = hookcore.RejectWrite
	RejectWriteWithContext = hookcore.RejectWriteWithContext
	AnnotateWrite          = hookcore.AnnotateWrite

	AcceptEdit            = hookcore.AcceptEdit
	AcceptEditWithInput   = hookcore.AcceptEditWithInput
	RejectEdit            = hookcore.RejectEdit
	RejectEditWithContext = hookcore.RejectEditWithContext
	AskBeforeEdit         = hookcore.AskBeforeEdit

	AcceptPrompt = hookcore.AcceptPrompt
	RejectPrompt = hookcore.RejectPrompt
	EnrichPrompt = hookcore.EnrichPrompt

	AcknowledgeFailure = hookcore.AcknowledgeFailure
	AnnotateFailure    = hookcore.AnnotateFailure
	RejectAfterFailure = hookcore.RejectAfterFailure

	AllowRead = hookcore.AllowRead
	DenyRead  = hookcore.DenyRead

	// Built-in scenario selectors.
	ScenarioFromArg = hookcore.ScenarioFromArg
	ScenarioFromEnv = hookcore.ScenarioFromEnv
)

Verdict constructors, re-exported from hookcore.

View Source
var Catalog = []CatalogEntry{

	{AgentClaude, "claude-stop", ".claude/settings.json", "Stop", StyleClaudeNested},
	{AgentClaude, "claude-pre-tool-use", ".claude/settings.json", "PreToolUse", StyleClaudeNested},
	{AgentClaude, "claude-pre-file-write", ".claude/settings.json", "PreToolUse", StyleClaudeNested},
	{AgentClaude, "claude-after-file-write", ".claude/settings.json", "PostToolUse", StyleClaudeNested},
	{AgentClaude, "claude-user-prompt-submit", ".claude/settings.json", "UserPromptSubmit", StyleClaudeNested},
	{AgentClaude, "claude-subagent-stop", ".claude/settings.json", "SubagentStop", StyleClaudeNested},
	{AgentClaude, "claude-post-tool-use-failure", ".claude/settings.json", "PostToolUseFailure", StyleClaudeNested},

	{AgentDroid, "droid-stop", ".factory/settings.json", "Stop", StyleClaudeNested},
	{AgentDroid, "droid-pre-tool-use", ".factory/settings.json", "PreToolUse", StyleClaudeNested},
	{AgentDroid, "droid-pre-file-write", ".factory/settings.json", "PreToolUse", StyleClaudeNested},
	{AgentDroid, "droid-after-file-write", ".factory/settings.json", "PostToolUse", StyleClaudeNested},
	{AgentDroid, "droid-user-prompt-submit", ".factory/settings.json", "UserPromptSubmit", StyleClaudeNested},
	{AgentDroid, "droid-subagent-stop", ".factory/settings.json", "SubagentStop", StyleClaudeNested},

	{AgentCursor, "cursor-stop", ".cursor/hooks.json", "stop", StyleFlatCommand},
	{AgentCursor, "cursor-before-shell", ".cursor/hooks.json", "beforeShellExecution", StyleFlatCommand},
	{AgentCursor, "cursor-before-mcp", ".cursor/hooks.json", "beforeMCPExecution", StyleFlatCommand},
	{AgentCursor, "cursor-before-file-write", ".cursor/hooks.json", "preToolUse", StyleFlatCommand},
	{AgentCursor, "cursor-after-file-edit", ".cursor/hooks.json", "postToolUse", StyleFlatCommand},
	{AgentCursor, "cursor-before-submit-prompt", ".cursor/hooks.json", "beforeSubmitPrompt", StyleFlatCommand},
	{AgentCursor, "cursor-before-file-read", ".cursor/hooks.json", "beforeReadFile", StyleFlatCommand},
	{AgentCursor, "cursor-before-read-file", ".cursor/hooks.json", "beforeReadFile", StyleFlatCommand},
	{AgentCursor, "cursor-subagent-stop", ".cursor/hooks.json", "subagentStop", StyleFlatCommand},
	{AgentCursor, "cursor-post-tool-use-failure", ".cursor/hooks.json", "postToolUseFailure", StyleFlatCommand},

	{AgentWindsurf, "windsurf-pre-run-command", ".codeium/windsurf/hooks.json", "pre_run_command", StyleFlatCommand},
	{AgentWindsurf, "windsurf-pre-mcp-tool-use", ".codeium/windsurf/hooks.json", "pre_mcp_tool_use", StyleFlatCommand},
	{AgentWindsurf, "windsurf-pre-user-prompt", ".codeium/windsurf/hooks.json", "pre_user_prompt", StyleFlatCommand},
	{AgentWindsurf, "windsurf-pre-read-code", ".codeium/windsurf/hooks.json", "pre_read_code", StyleFlatCommand},
	{AgentWindsurf, "windsurf-pre-write-code", ".codeium/windsurf/hooks.json", "pre_write_code", StyleFlatCommand},
	{AgentWindsurf, "windsurf-post-write-code", ".codeium/windsurf/hooks.json", "post_write_code", StyleFlatCommand},
	{AgentWindsurf, "windsurf-post-cascade-response", ".codeium/windsurf/hooks.json", "post_cascade_response", StyleFlatCommand},

	{AgentGemini, "gemini-before-tool", ".gemini/settings.json", "BeforeTool", StyleGeminiNested},
	{AgentGemini, "gemini-before-file-tool", ".gemini/settings.json", "BeforeTool", StyleGeminiNested},
	{AgentGemini, "gemini-after-agent", ".gemini/settings.json", "AfterAgent", StyleGeminiNested},
	{AgentGemini, "gemini-before-agent", ".gemini/settings.json", "BeforeAgent", StyleGeminiNested},
	{AgentGemini, "gemini-after-file-tool", ".gemini/settings.json", "AfterTool", StyleGeminiNested},

	{AgentCopilotCLI, "copilot-cli-stop", ".copilot/hooks/agenthooks.json", "Stop", StyleCopilotCLINested},
	{AgentCopilotCLI, "copilot-cli-pre-tool-use", ".copilot/hooks/agenthooks.json", "PreToolUse", StyleCopilotCLINested},
	{AgentCopilotCLI, "copilot-cli-pre-file-write", ".copilot/hooks/agenthooks.json", "PreToolUse", StyleCopilotCLINested},
	{AgentCopilotCLI, "copilot-cli-after-file-write", ".copilot/hooks/agenthooks.json", "PostToolUse", StyleCopilotCLINested},
	{AgentCopilotCLI, "copilot-cli-post-tool-use-failure", ".copilot/hooks/agenthooks.json", "PostToolUseFailure", StyleCopilotCLINested},
	{AgentCopilotCLI, "copilot-cli-subagent-stop", ".copilot/hooks/agenthooks.json", "SubagentStop", StyleCopilotCLINested},
	{AgentCopilotCLI, "copilot-cli-user-prompt-submit", ".copilot/hooks/agenthooks.json", "UserPromptSubmit", StyleCopilotCLINested},
}

Catalog lists every unified route and where each platform expects it installed.

The VS Code Copilot extension is intentionally absent: its hooks are registered per-project in .github/hooks/*.json (not a home-directory settings file), so `agenthooks install` does not write them automatically — see the README for manual setup. The GitHub Copilot CLI (a distinct product) IS installable via its user-level ~/.copilot/hooks/ directory and is listed below.

Functions

func AddRoute

func AddRoute(name string, fn RouteFunc)

AddRoute registers fn under the given command name. When Dispatch is called and os.Args[1] matches name, fn is invoked.

func AfterFileWrite

func AfterFileWrite(fn FileWriteFunc)

AfterFileWrite registers the default handler for post-file-edit events:

  • Claude Code → "claude-after-file-write"
  • Cursor → "cursor-after-file-write" (postToolUse; delivers additional_context, cannot block)
  • Windsurf → "windsurf-post-write-code" (fire-and-forget)
  • Factory Droid → "droid-after-file-write"
  • Gemini CLI → "gemini-after-file-tool"
  • VS Code Copilot → "copilot-after-file-write"

func AfterFileWriteScenario

func AfterFileWriteScenario(name string, fn FileWriteFunc)

AfterFileWriteScenario registers a named scenario handler for the post-file-write hook.

func AfterToolFailure

func AfterToolFailure(fn ToolFailureFunc)

AfterToolFailure registers the default handler for failed tool calls:

  • Claude Code → "claude-post-tool-use-failure" (can block / annotate)
  • Cursor → "cursor-post-tool-use-failure" (observational; verdict ignored)

func AfterToolFailureScenario

func AfterToolFailureScenario(name string, fn ToolFailureFunc)

AfterToolFailureScenario registers a named scenario handler for the tool-failure hook.

func BeforeFileEdit

func BeforeFileEdit(fn FileEditFunc)

BeforeFileEdit registers the default handler for "agent about to write/edit a file" events — a pre-write GATE that can BLOCK the change before it lands (unlike AfterFileWrite, which fires post-write and can only annotate). The handler sees the proposed FilePath/Changes and returns a PreToolUse-style verdict (AcceptEdit / RejectEdit / RejectEditWithContext / AskBeforeEdit):

  • Claude Code → "claude-pre-file-write" (PreToolUse, Write/Edit/MultiEdit; deny + additionalContext)
  • Factory Droid → "droid-pre-file-write" (PreToolUse, Write/Edit; deny only — no context channel)

func BeforeFileEditScenario

func BeforeFileEditScenario(name string, fn FileEditFunc)

BeforeFileEditScenario registers a named scenario handler for the pre-file-write hook.

func BeforeFileRead

func BeforeFileRead(fn FileReadFunc)

BeforeFileRead registers the default handler for "agent about to read a file" events:

  • Cursor → "cursor-before-read-file" (allow/deny)
  • Windsurf → "windsurf-pre-read-code" (blocking via exit 2)

func BeforeFileReadScenario

func BeforeFileReadScenario(name string, fn FileReadFunc)

BeforeFileReadScenario registers a named scenario handler for the pre-file-read hook.

func BeforePrompt

func BeforePrompt(fn PromptFunc)

BeforePrompt registers the default handler for prompt-submission events:

  • Claude Code → "claude-user-prompt-submit"
  • Cursor → "cursor-before-submit-prompt"
  • Windsurf → "windsurf-pre-user-prompt" (blocking via exit 2)
  • Factory Droid → "droid-user-prompt-submit"
  • Gemini CLI → "gemini-before-agent"
  • VS Code Copilot → "copilot-user-prompt-submit"

func BeforePromptScenario

func BeforePromptScenario(name string, fn PromptFunc)

BeforePromptScenario registers a named scenario handler for the prompt-submit hook.

func BeforeToolCall

func BeforeToolCall(fn ToolCallFunc)

BeforeToolCall registers the default handler for pre-execution events:

  • Claude Code → "claude-pre-tool-use"
  • Cursor → "cursor-before-shell", "cursor-before-mcp"
  • Windsurf → "windsurf-pre-run-command", "windsurf-pre-mcp-tool-use" (blocking via exit 2)
  • Factory Droid → "droid-pre-tool-use" (blocking via exit 2)
  • Gemini CLI → "gemini-before-tool"
  • VS Code Copilot → "copilot-pre-tool-use"

func BeforeToolCallScenario

func BeforeToolCallScenario(name string, fn ToolCallFunc)

BeforeToolCallScenario registers a named scenario handler for the pre-tool-call hook.

func ClearRoutes

func ClearRoutes()

ClearRoutes removes all registered handlers and scenario state. Intended for use in tests.

func Dispatch

func Dispatch()

Dispatch selects and runs the handler whose name matches os.Args[1]. If os.Args[1] is absent, the binary name is used instead. Exits with code 1 if no matching handler is found.

func Process

func Process[I any, O any](handler func(I) O)

Process reads JSON from stdin, passes it to handler, and writes the result to stdout. Any stdin parse error causes a graceful exit (code 0) so a bad payload never blocks an agent. The implementation lives in internal/hookcore so platform adapters share it.

func ProcessE

func ProcessE[I any, O any](handler func(I) (O, error))

ProcessE is like Process but allows the handler to signal a blocking error. When handler returns a non-nil error, agenthooks writes the message to stderr and exits with code 2, which causes supporting agents to surface the message and block the pending action.

func RouteNames

func RouteNames() []string

RouteNames returns the names of all currently registered routes, unsorted. Intended for inspection and tests (e.g. verifying the route Catalog stays in sync with what the unified handlers register).

func UseScenarioSelector

func UseScenarioSelector(sel ScenarioSelector)

UseScenarioSelector overrides how the active scenario key is chosen for all hooks. A nil selector is ignored. The default is ScenarioFromArg.

func WhenAgentIdle

func WhenAgentIdle(fn AgentIdleFunc)

WhenAgentIdle registers the default handler for "agent finished responding" events:

  • Claude Code → "claude-stop"
  • Cursor → "cursor-stop"
  • Windsurf → "windsurf-post-cascade-response" (fire-and-forget; Interrupt logged, ignored)
  • Factory Droid → "droid-stop"
  • Gemini CLI → "gemini-after-agent"
  • VS Code Copilot → "copilot-stop"

func WhenAgentIdleScenario

func WhenAgentIdleScenario(name string, fn AgentIdleFunc)

WhenAgentIdleScenario registers a named scenario handler for the agent-idle hook.

func WhenSubagentIdle

func WhenSubagentIdle(fn AgentIdleFunc)

WhenSubagentIdle registers the default handler for "subagent finished" events. It reuses AgentIdleEvent/IdleVerdict; Interrupt blocks the subagent from stopping (on Cursor it auto-submits a follow-up):

  • Claude Code → "claude-subagent-stop"
  • Factory Droid → "droid-subagent-stop"
  • VS Code Copilot → "copilot-subagent-stop"
  • Cursor → "cursor-subagent-stop"

func WhenSubagentIdleScenario

func WhenSubagentIdleScenario(name string, fn AgentIdleFunc)

WhenSubagentIdleScenario registers a named scenario handler for the subagent-idle hook.

Types

type AgentID

type AgentID = hookcore.AgentID

type AgentIdleEvent

type AgentIdleEvent = hookcore.AgentIdleEvent

type AgentIdleFunc

type AgentIdleFunc = hookcore.AgentIdleFunc

type CatalogEntry

type CatalogEntry struct {
	Agent       AgentID   // which platform this route belongs to
	Route       string    // the route name passed as the hook binary's first argument
	SettingsRel string    // settings-file path relative to the user's home directory (slash-separated)
	EventKey    string    // key under which the hook is registered in the settings file
	Style       HookStyle // how the entry is encoded in that file
}

CatalogEntry maps one unified hook route to the settings-file slot a platform expects it in. It is the single source of truth shared by route registration (the WhenAgentIdle / BeforeToolCall / ... functions) and `agenthooks install`.

type FileDiff

type FileDiff = hookcore.FileDiff

type FileEditEvent

type FileEditEvent = hookcore.FileEditEvent

type FileEditFunc

type FileEditFunc = hookcore.FileEditFunc

type FileEditVerdict

type FileEditVerdict = hookcore.FileEditVerdict

type FileReadEvent

type FileReadEvent = hookcore.FileReadEvent

type FileReadFunc

type FileReadFunc = hookcore.FileReadFunc

type FileReadVerdict

type FileReadVerdict = hookcore.FileReadVerdict

type FileWriteEvent

type FileWriteEvent = hookcore.FileWriteEvent

type FileWriteFunc

type FileWriteFunc = hookcore.FileWriteFunc

type FileWriteVerdict

type FileWriteVerdict = hookcore.FileWriteVerdict

type HookStyle

type HookStyle string

HookStyle describes how a platform's settings file encodes a hook entry.

const (
	// StyleClaudeNested writes {"hooks": {EventKey: [{"type":"command","command":...}]}}.
	// Used by Claude Code and Factory Droid.
	StyleClaudeNested HookStyle = "claude-nested"
	// StyleFlatCommand writes {EventKey: {"command": ...}} at the top level.
	// Used by Cursor and Windsurf Cascade.
	StyleFlatCommand HookStyle = "flat-command"
	// StyleGeminiNested writes {"hooks": {EventKey: [{"matcher":"","hooks":[{"type":"command","command":...}]}]}}.
	// Used by Gemini CLI.
	StyleGeminiNested HookStyle = "gemini-nested"
	// StyleCopilotCLINested writes {"version":1,"hooks":{EventKey:[{"type":"command","command":...}]}}.
	// Used by GitHub Copilot CLI (a dedicated hooks file under ~/.copilot/hooks/).
	StyleCopilotCLINested HookStyle = "copilot-cli-nested"
)

type IdleVerdict

type IdleVerdict = hookcore.IdleVerdict

type PromptEvent

type PromptEvent = hookcore.PromptEvent

type PromptFunc

type PromptFunc = hookcore.PromptFunc

type PromptVerdict

type PromptVerdict = hookcore.PromptVerdict

type RouteFunc

type RouteFunc func()

RouteFunc is the type for handlers registered via AddRoute.

type ScenarioMeta

type ScenarioMeta = hookcore.ScenarioMeta

type ScenarioSelector

type ScenarioSelector = hookcore.ScenarioSelector

type ToolCallEvent

type ToolCallEvent = hookcore.ToolCallEvent

type ToolCallFunc

type ToolCallFunc = hookcore.ToolCallFunc

type ToolFailureEvent

type ToolFailureEvent = hookcore.ToolFailureEvent

type ToolFailureFunc

type ToolFailureFunc = hookcore.ToolFailureFunc

type ToolFailureVerdict

type ToolFailureVerdict = hookcore.ToolFailureVerdict

type ToolKind

type ToolKind = hookcore.ToolKind

type ToolVerdict

type ToolVerdict = hookcore.ToolVerdict

Directories

Path Synopsis
Package claude provides types and response helpers for Claude Code hooks.
Package claude provides types and response helpers for Claude Code hooks.
cmd
agenthooks command
Command agenthooks provides install, build, and init utilities for agent hooks.
Command agenthooks provides install, build, and init utilities for agent hooks.
Package copilot provides types and response helpers for GitHub Copilot agent hooks in Visual Studio Code.
Package copilot provides types and response helpers for GitHub Copilot agent hooks in Visual Studio Code.
Package copilotcli provides types and response helpers for GitHub Copilot CLI (and Copilot cloud agent) hooks.
Package copilotcli provides types and response helpers for GitHub Copilot CLI (and Copilot cloud agent) hooks.
Package cursor provides types and response helpers for Cursor IDE hooks.
Package cursor provides types and response helpers for Cursor IDE hooks.
Package droid provides types and response helpers for Factory Droid hooks.
Package droid provides types and response helpers for Factory Droid hooks.
examples
copilot-demo command
copilot-demo is a runnable hook binary for the VS Code Copilot demo.
copilot-demo is a runnable hook binary for the VS Code Copilot demo.
Package gemini provides types and response helpers for Gemini CLI hooks.
Package gemini provides types and response helpers for Gemini CLI hooks.
Package install writes per-agent hook configuration into each agent's settings file.
Package install writes per-agent hook configuration into each agent's settings file.
internal
codec
Package codec handles JSON serialization between hook processes and AI agents.
Package codec handles JSON serialization between hook processes and AI agents.
hookcore
Package hookcore defines the platform-agnostic vocabulary of the unified hook API: the event views, verdicts, and constructors shared across every agent.
Package hookcore defines the platform-agnostic vocabulary of the unified hook API: the event views, verdicts, and constructors shared across every agent.
scaffold
Package scaffold generates a starter hooks project from built-in templates.
Package scaffold generates a starter hooks project from built-in templates.
Package windsurf provides types and response helpers for Windsurf Cascade hooks.
Package windsurf provides types and response helpers for Windsurf Cascade hooks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL