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
  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:

.reflex/config.json
{
  "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.

On this page