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
options?: Record<string, unknown>; // config passed to this reflex (see below)
}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"]
}A reflex that takes configuration declares it under an optional options key in the manifest, so it's documented and editor-validated.
The pack.json manifest
A pack bundles capabilities — MCP servers, skills, session hooks, reflexes — plus the secrets and options they're configured with. parsePackManifest in @agentreflex/core validates this shape; installs fail loudly on anything malformed.
{
"$schema": "https://agentreflex.dev/schema/pack-v1.json",
"name": "acme", // kebab-case, required
"title": "Acme",
"description": "Acme's tools, instructions, and session context.",
"version": "0.1.0",
"license": "Apache-2.0",
"homepage": "https://acme.dev",
"category": "search", // registry category
"secrets": {
"acme_token": { "title": "Acme API token", "description": "acme.dev → Settings → Tokens", "required": true }
},
"options": {
"acme_url": { "title": "MCP endpoint", "default": "https://api.acme.dev/mcp" }
},
"mcp": {
"acme": {
"type": "http", // http is the supported transport
"url": "${options.acme_url}",
"headers": { "Authorization": "Bearer ${secrets.acme_token}" }
}
},
"skills": [{ "name": "acme-usage", "source": "skills/acme-usage" }],
"hooks": [{ "event": "SessionStart", "run": "hooks/session-context.mjs", "timeout": 15 }],
"reflexes": [{ "source": "reflexes/guard.mjs", "with": { "level": "strict" } }],
"agents": ["claude"] // optional targeting; absent = every capable agent
}Rules: name is kebab-case; every source/run resolves inside the pack (no ..); ${secrets.x} / ${options.x} references are interpolated at install time and unresolved references throw; hook events are SessionStart and UserPromptSubmit. Adapters carry pack capabilities through the optional Adapter.pack writer set (mcp / skill / lifecycleHook + removals) — an adapter without a writer skips that capability, reported, never fatal.
Configuration
A reflex can be configured by whoever installs it. The .reflex/config.json entry becomes an object with a with block, and the reflex reads it from ctx.options:
{
"reflexes": [
"./no-secrets.mjs",
{ "source": "./my-reflex.mjs", "with": { "allow": ["src/**"] } }
]
}onToolCall(ctx) {
const allow = (ctx.options?.allow as string[]) ?? [];
// …
}A plain string entry ("./x.mjs") is shorthand for { "source": "./x.mjs" } with no options. Options live under with; a top-level key in config.json is not read.
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.
This is the machine path — silent and fail-open by design. To test a reflex, don't invoke hook by hand; use arx dev, which simulates a tool call and prints a readable verdict.