Skip to content

run

ts
import { run } from "@pivanov/agent-hooks-bridge";

Reads stdin, resolves the host, parses the native payload into a unified event, calls your handler, serializes the response, sets the exit code.

Signature

ts
const run: (handlers: THandlerMap, options?: IRunOptions) => Promise<void>;

interface IRunOptions {
  host?: "claude" | "cursor" | "codex";
  stdin?: string;
  argv?: readonly string[];
  exit?: (code: number) => never;
  write?: (chunk: string) => void;
  maxBytes?: number;
  cursorAskFallback?: "allow" | "deny" | "ask";
}

Defaults

ts
await run(hooks);

Equivalent to:

  • host: resolved from options.host, then --host <id> in process.argv, then stdin detection
  • stdin: read from process.stdin until EOF, capped at 1 MiB
  • argv: process.argv.slice(2)
  • exit: process.exit
  • write: process.stdout.write
  • maxBytes: 1_048_576
  • cursorAskFallback: "deny" (the Cursor ask regression that started in 2.4.21 and is still present on 3.2.16 auto-downgrades)

cursorAskFallback

Cursor's permission: "ask" has been broken since 2.4.21 and is still broken on 3.2.16 (last verified). The visible behavior changed across the regression window: 2.4.21 through 2.x silently treats ask as deny; 3.x silently treats it as allow. Either way the hook author's intent is dropped.

By default the bridge reads cursor_version from stdin and downgrades decision: "ask" to "deny" on every version >= 2.4.21, with a stderr warning. That's the safer default on both eras. Pass { cursorAskFallback: "ask" } to opt out and let ask pass through unchanged (it will still be silently mishandled, but the bridge stays out of the way):

ts
await run(hooks, { cursorAskFallback: "ask" });

The downgrade only fires on Cursor permission events (beforeShellExecution, beforeMCPExecution, beforeReadFile, preToolUse). Other events are unaffected by this option.

Host resolution order

  1. options.host if provided
  2. --host <id> token in options.argv (or process.argv)
  3. Stdin scoring (see Host Detection)

If none resolve, run() writes a stderr error and exits 1.

Forcing a host

ts
await run(hooks, { host: "claude" });

Programmatic use (testing, embedding)

stdin, argv, stdout writes, and exit are all injectable.

ts
let captured = "";
let exitCode = -1;

await run(hooks, {
  stdin: JSON.stringify(payload),
  argv: ["--host", "claude"],
  write: (chunk) => {
    captured += chunk;
  },
  exit: ((code: number) => {
    exitCode = code;
    throw new Error("__exit__");
  }) as never,
}).catch((err) => {
  if (!(err instanceof Error) || err.message !== "__exit__") throw err;
});

assert.equal(JSON.parse(captured).hookSpecificOutput.permissionDecision, "deny");
assert.equal(exitCode, 2);

process.exit is (code) => never. The override should also unwind the stack; throwing a sentinel and catching it keeps test code linear.

Exit codes

OutcomeExit
Handler returns decision: "deny"2
Handler returns "allow" / "ask" / nothing0
No handler registered for the event0
stdin parse failure1
Host resolution failure1
Adapter parse / serialize throws1
Handler throws1

stderr always carries a [agent-hooks-bridge] … line for any non-zero exit.

Stdout protocol

stdout carries the host's wire JSON only. The bridge writes:

  • One JSON object followed by \n, or
  • Nothing (when the response is empty).

Logs, prompts, ANSI sequences, and any non-JSON output never appear on stdout. All human-facing output goes to stderr via the internal warn() helper.

Stderr channel

[agent-hooks-bridge]-prefixed lines on stderr cover dropped fields, version-specific regressions, and bridge errors. Warnings don't affect exit code; only the conditions in the table above do.

Example successful run with a dropped field:

text
$ ./hook.ts --host cursor < cursor-prompt-payload.json
{"continue":true,"user_message":"..."}
[agent-hooks-bridge] 'additional_context' is not supported on cursor's beforeSubmitPrompt response; dropped; cursor's prompt-submit event cannot inject context; consider sessionStart
$ echo $?
0

stdout reaches the host. stderr reaches the agent logs and the user.

Not affiliated with Anthropic, Anysphere, or OpenAI. Supported by LogicStar AI.