agentreflex
Reference

Specification

The reflex module interface, the context and decision types, and the reflex.json manifest.

A reflex is defined by a small TypeScript contract from @agentreflex/core. Everything else — the per-agent hooks, the catalog — is built around it.

The reflex module

A reflex's default export implements Reflex:

interface Reflex {
  name: string;
  onToolCall?(ctx: ToolCallContext): Decision | Promise<Decision>;
  onToolResult?(ctx: ToolResultContext): void | Promise<void>;
}

Context

onToolCall receives the call before it runs; onToolResult receives it after.

interface ToolCallContext {
  event: "onToolCall";
  agent: "claude" | "cursor" | "gemini" | "copilot" | "windsurf" | "opencode" | "codex";
  tool: string;        // normalized: "Bash" | "Edit" | "Write" | "Read" | …
  command?: string;    // present when the tool is a shell
  paths: string[];     // files the tool would touch
  cwd: string;
  raw: unknown;        // the original agent payload, untouched
}

ToolResultContext has the same shape with event: "onToolResult".

Decision

onToolCall returns one of:

type Decision =
  | { action: "pass" }
  | { action: "deny"; reason: string }
  | { action: "ask"; reason: string }
  | { action: "modify"; args: Record<string, unknown>; reason?: string };

Helpers build them: pass(), deny(reason), ask(reason), modify(args, reason?). Reflexes evaluate in order; the first non-pass wins.

The reflex.json manifest

A distributable reflex ships a manifest describing itself for the catalog:

{
  "$schema": "https://agentreflex.dev/schema/reflex-v1.json",
  "name": "no-force-push",
  "title": "No force-push",
  "description": "Blocks git push --force on shared branches.",
  "version": "0.0.0",
  "license": "MIT",
  "author": "agentreflex",
  "official": true,
  "events": ["onToolCall"],
  "capabilities": { "decisions": ["deny"], "reads": ["command"] },
  "entry": "dist/index.js",
  "tags": ["git", "safety", "protective"]
}

The agent hook

Each wired agent calls arx hook --agent <name> before a tool runs. The dispatcher reads the agent's native payload on stdin, normalizes it to a ToolCallContext, runs your reflexes, and writes the decision back in that agent's native response format — exit code, stderr, or JSON, depending on the agent. Per-agent translation lives in the adapters, so a reflex never has to know which agent it's running under.

On this page