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.
go get github.com/Checkmarx/ast-cx-hooksZero dependencies (standard library only).
| 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.
8 unified hook categories map automatically to each platform's native events.
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)
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)
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)
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)
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)
Reuses AgentIdleEvent / IdleVerdict; Interrupt blocks the subagent from stopping.
Verdicts: Resume() · Interrupt(feedback)
agenthooks.AfterToolFailure(func(e agenthooks.ToolFailureEvent) agenthooks.ToolFailureVerdict {
return agenthooks.AnnotateFailure("Tool " + e.ToolName + " failed: " + e.Error)
})Verdicts: AcknowledgeFailure() · AnnotateFailure(note) · RejectAfterFailure(reason)
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)
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).
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.
# 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 ./myhookinstall 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.
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).
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-useUnit-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")
}
}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.*.
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()
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. installis non-destructive. It merges into existing settings (preserving your other hooks), aborts rather than overwriting a file it cannot parse, and writes a.bakfirst.
- Usage Guide — per-agent payloads, routes, and testing hooks locally.
- Troubleshooting — common build, install, and runtime issues.
- Changelog — release history.
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.
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.
Apache 2.0 — see LICENSE.txt for details.
Website: Checkmarx.
© 2026 Checkmarx Ltd. All Rights Reserved.