Skip to content
lfox91Public
forked from polyhook/polyhook

About

Write AI coding agent hooks once. Run them everywhere. Multi-language SDK (TS/Rust/Go/Python/.NET) over a shared WASM core — supports Claude Code, Cursor, Windsurf, Cline, and Amp.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

polyhook

polyhook

Write AI coding agent hooks once. Run them everywhere.

polyhook is a multi-language SDK for AI coding agent hooks. Instead of parsing each tool's bespoke stdin/stdout format, you call polyhook — it detects which tool invoked your binary, deserializes the event into a normalized struct, and serializes your response back in the format that tool expects.

Your hook binary runs unchanged whether Claude Code, Cursor, Windsurf, Cline, Amp, or Pi invoked it.


Architecture

Rust core compiled to WASM. Every language SDK is a thin shim over the same binary — no logic re-implemented per language. See ARCHITECTURE.md.


The Problem

Every AI coding tool triggers hooks differently. The same logical event — "bash command about to run" — arrives in a different shape and expects a different response format:

Claude Code  →  stdin: { "tool_name": "Bash", "tool_input": { "command": "..." } }
                stdout: { "decision": "block", "reason": "..." }

Cursor       →  stdin: { "type": "BeforeToolCall", "toolCall": { "name": "run_terminal_cmd", "args": {...} } }
                stdout: { "action": "deny", "message": "..." }

Windsurf     →  stdin: { "event": "pre_tool", "tool": "run_command", "parameters": {...} }
                stdout: { "allow": false, "reason": "..." }

Without polyhook you write a parser and serializer for each tool. With polyhook you call read() and respond().


How It Works

AI tool  ──stdin──▶  your binary
                         │
                    polyhook.read()        ← WASM: detects tool, parses format
                         │
                    your hook logic        ← tool-agnostic
                         │
                    polyhook.respond()     ← WASM: serializes to tool's format
                         │
your binary  ──stdout──▶  AI tool

Normalized Types

Types are auto-generated in every SDK from core/schema.json — not hand-written. See ARCHITECTURE.md for the full generation pipeline.

HookEvent

interface HookEvent {
  event:     "tool:before" | "tool:after" | "session:start" | "session:stop" | "turn:stop" | "agent:stop" | "prompt:submit" | "notification";
  tool?:     string;                        // normalized tool name, e.g. "bash", "write_file"
  input?:    Record<string, unknown>;       // tool input arguments
  prompt?:   string;                        // user prompt (prompt:submit only)
  reply?:    string;                        // agent's final reply text (turn:stop only)
  transcriptPath?: string;                  // session transcript file, when the caller sends one
  output?:   Record<string, unknown>;       // tool output (tool:after only)
  sessionId: string;
  agentId?:  string;
  caller:    "claude-code" | "cursor" | "windsurf" | "cline" | "amp" | "gemini-cli" | "hermes" | "pi" | "codex" | "unknown";
}

Normalized tool names: docs/tool-names.md

HookResponse

type HookResponse =
  | { action: "approve" }
  | { action: "block";  message: string }
  | { action: "modify"; input: Record<string, unknown> }
  | { action: "context"; context: string }   // prompt:submit: add text to the model's context

Language SDKs

All SDKs expose the same two functions. The WASM module does all the work.

TypeScript / JavaScript

npm install @polyhook/sdk

Examples: packages/sdk-ts/examples/

Rust

The only SDK that links core natively — no WASM overhead.

cargo add polyhook

Examples: packages/sdk-rust/examples/

Go

go get github.com/tupe12334/polyhook/packages/sdk-go

Examples: packages/sdk-go/examples/

C# / .NET

dotnet add package Polyhook.Sdk

Examples: packages/sdk-dotnet/examples/

Python

pip install polyhook

Examples: packages/sdk-python/examples/

Any language with a WASM runtime can bind polyhook. See BINDINGS.md for the raw WASM API.


Supported Tools

Tool Status
Claude Code ✅ Supported
Cursor ✅ Supported
Windsurf ✅ Supported
Cline ✅ Supported
Amp ✅ Supported
Gemini CLI ✅ Supported
Hermes Agent ✅ Supported
Pi ✅ Supported
Codex ✅ Supported
Continue 🚧 In progress
Aider 🚧 In progress
Copilot 📋 Planned

prompt:submit context injection ({ action: "context" }) works on Claude Code, Codex, Gemini CLI (BeforeAgent), Hermes (pre_llm_call) and Cline (UserPromptSubmit). Cursor, Windsurf and Amp have no prompt hook that can add context, so there a context response is sent as approve.

Pi uses the Claude Code-compatible hook payload format; set POLYHOOK_CALLER=pi if you want it labeled explicitly.

Source of truth: tools.toml — hook documentation links: docs/tool-names.md


Design Goals

  • One implementation. All detection and serialization logic lives in core. Language SDKs cannot diverge.
  • Runtime only. No code generation. polyhook runs inside your binary at hook invocation time.
  • Transparent fallback. Unknown caller → caller: "unknown", best-effort parse. Your logic keeps running.
  • Bring your own runtime. Each SDK ships polyhook.wasm and a thin host binding. Swap the WASM runtime if your platform requires it.

Contributing

See CONTRIBUTING.md. To add tool support, add detection heuristics and mappings to core — all language SDKs pick it up automatically on the next WASM build.


License

MIT

About

Write AI coding agent hooks once. Run them everywhere. Multi-language SDK (TS/Rust/Go/Python/.NET) over a shared WASM core — supports Claude Code, Cursor, Windsurf, Cline, and Amp.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages