npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pivanov/agent-hooks-bridge

v0.1.2

Published

One hook script, every agent. Translates between Claude Code, Cursor, and Codex hook schemas.

Readme

@pivanov/agent-hooks-bridge

One hook script, every agent. Translates between Claude Code, Cursor, and Codex hook schemas at runtime.

Each agent ships its own incompatible hook system. This package lets you write one hook that runs across all three.

  • Zero runtime dependencies. TypeScript. MIT.
  • ESM-only. Bun and Node 22+.
  • Subpath exports per host (/claude, /cursor, /codex) for tree-shaking.
  • Library API. No global CLI.

Install

bun add @pivanov/agent-hooks-bridge

Quick start

Write one hook in your repo:

#!/usr/bin/env bun
// .hooks/format.ts
import { defineHook, run } from "@pivanov/agent-hooks-bridge";

const hooks = defineHook({
  PreToolUse: (event) => {
    if (event.tool === "Bash" && /rm -rf/.test(String(event.tool_input.command ?? ""))) {
      return { decision: "deny", reason: "rm -rf is blocked by policy" };
    }
    return { decision: "allow" };
  },

  PostToolUse: async (event) => {
    if (event.tool !== "Edit" && event.tool !== "Write") {
      return { decision: "allow" };
    }
    const file = event.tool_input.file_path;
    if (typeof file !== "string" || !/\.(ts|tsx|js|jsx)$/.test(file)) {
      return { decision: "allow" };
    }
    await Bun.spawn(["biome", "check", "--write", file]).exited;
    return { decision: "allow", additional_context: `biome formatted ${file}` };
  },
});

await run(hooks);

chmod +x .hooks/format.ts, then wire it into all three host configs:

bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts

This generates or merges:

  • .claude/settings.json
  • .cursor/hooks.json
  • .codex/hooks.toml

Each entry runs the same script with --host <host> so the runtime knows which adapter to use without sniffing stdin.

Supported events

| Unified | Claude | Cursor | Codex | | ------------------ | ------------------ | -------------------------------------------------------------------------- | ------------------ | | SessionStart | SessionStart | sessionStart | SessionStart | | PreToolUse | PreToolUse | preToolUse, beforeShellExecution, beforeMCPExecution, beforeReadFile | PreToolUse | | PostToolUse | PostToolUse | postToolUse, afterShellExecution, afterMCPExecution, afterFileEdit | PostToolUse | | UserPromptSubmit | UserPromptSubmit | beforeSubmitPrompt | UserPromptSubmit | | Stop | Stop | stop | Stop |

Tool aliasing on PreToolUse / PostToolUse:

| Unified event.tool | Claude | Cursor | Codex | | -------------------- | ---------------- | ------------------------------- | ------------- | | Bash | Bash | shell events | Bash | | Edit | Edit | file edit events | apply_patch | | Write | Write | file edit events | apply_patch (collapsed to Edit) | | Read | Read | beforeReadFile | Read | | MCP | passthrough | MCP events | passthrough |

Cursor's specific events fold upward into the unified shape. The native payload is preserved at event._native.

Codex collapses apply_patch to event.tool === "Edit" regardless of whether it's a create or an update, because the native event carries no distinction. To disambiguate, branch on event.tool_input.patch (Codex emits a single *** Update File: / *** Add File: directive per call) or read event.native_tool (always set to the host's original tool name).

Response shape

interface IHookResponse {
  decision?: "allow" | "deny" | "ask";
  reason?: string;             // model-facing message
  user_message?: string;       // human-facing message (honored on Cursor permission events)
  modified_input?: object;     // tool-input mutation (Claude PreToolUse, Cursor preToolUse)
  additional_context?: string; // model context injection
}

Fields a host can't honor are dropped with a stderr warning naming the field, host, and event.

Per-host capability matrix

| Field | Claude | Cursor | Codex | | -------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | decision: "allow" | yes | yes (permission events; continue: true on beforeSubmitPrompt) | yes | | decision: "deny" | yes (exit 2) | yes (exit 2) | yes (exit 2) | | decision: "ask" | yes | downgraded to deny on Cursor 2.4.21+ (still broken on 3.2.16; pass cursorAskFallback: "ask" to opt out) | yes | | reason | permissionDecisionReason / reason | agent_message on permission events; dropped on beforeSubmitPrompt | permissionDecisionReason / reason | | user_message | dropped | user_message on permission events | dropped | | additional_context | SessionStart, PreToolUse, UserPromptSubmit, PostToolUse | sessionStart, postToolUse; dropped on beforeSubmitPrompt | SessionStart, UserPromptSubmit, PostToolUse; systemMessage on Stop | | modified_input | PreToolUse (as updatedInput); dropped elsewhere | preToolUse (as updated_input); dropped on specific events | dropped |

Cursor beforeSubmitPrompt

Cursor's prompt-submit response is { continue, user_message } only. additional_context returned on UserPromptSubmit works on Claude and Codex but is dropped on Cursor. Use SessionStart for project-level briefings or PostToolUse for between-turn context.

API

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

defineHook(handlers)

Type-checked builder for the event handler map. Identity at runtime.

run(handlers, options?)

Reads stdin, identifies the host, parses the native payload into a unified event, calls your handler, serializes the response, exits with the right code.

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

host precedence: explicit host option, then --host <id> in argv, then stdin detection.

Exit codes

  • 0: proceed (allow, ask, no decision)
  • 2: block (decision: "deny")
  • 1: bridge error (parse failure, unknown host, handler exception)

Subpath exports

For embedders who plug a single adapter into a custom transport:

import { claudeAdapter } from "@pivanov/agent-hooks-bridge/claude";

const event = claudeAdapter.parse(rawStdin);
const native = claudeAdapter.serialize({ decision: "deny" }, event);

Adapter contract:

interface IHostAdapter {
  readonly id: "claude" | "cursor" | "codex";
  readonly capabilities: ReadonlySet<TUnifiedEventName>;
  parse(raw: Record<string, unknown>): TUnifiedEvent;
  serialize(response: IHookResponse, event: TUnifiedEvent): INativeResponse;
}

install

bunx @pivanov/agent-hooks-bridge install <script> [options]

Options:
  --events <list>     comma-separated unified events (default: all 5)
  --hosts <list>      comma-separated hosts (default: claude,cursor,codex).
                      Pass 'auto' to write only to hosts whose config dir
                      exists on this machine.
  --matcher <pattern> tool matcher for PreToolUse / PostToolUse (default: *)
  --dry-run           print the config diff without writing
  --cwd <path>        install relative to this directory
  --global, -g        write to each host's user-level config:
                        claude  ~/.claude/settings.json
                        cursor  ~/.cursor/hooks.json
                        codex   $CODEX_HOME/hooks.toml (default ~/.codex/)
                      Overrides --cwd and --config-root.
  --config-root       treat <cwd> as the host's config dir directly (escape
                      hatch for layouts like ~/.claude-main, ~/.claude-ship)
  --help, -h          show usage

Re-runs are idempotent: only entries pointing at the same script are replaced. Other entries in the same config file are preserved. --dry-run prints the would-be configs to stdout; nothing on disk changes.

For Codex TOML, the bridge writes a marker block (# >>> agent-hooks-bridge … # <<< agent-hooks-bridge); re-installs replace the block in place.

To remove the entries:

bunx @pivanov/agent-hooks-bridge uninstall ./.hooks/format.ts

uninstall (and doctor) accept the same --hosts, --cwd, --global, and --config-root flags. Only entries whose command matches the given script (with or without a trailing --host <id>) are removed. Other entries in the same config are preserved. Empty event arrays are dropped; for Codex, the managed block is removed in place.

Troubleshooting

Bridge diagnostics go to stderr, prefixed with [agent-hooks-bridge].

'<field>' is not supported on <host>'s <event> response; dropped

Your handler returned a field the host can't surface for that event. Common cases:

  • additional_context on Cursor's beforeSubmitPrompt: Cursor's prompt-submit response is allow/deny only. Move to SessionStart.
  • user_message on Claude or Codex: those hosts have no human-facing channel. Use reason (model-facing).
  • modified_input on Cursor's specific events: only the generic preToolUse accepts updated_input.

cursor <version>: 'permission: "ask"' is not honored ...; downgrading to 'deny'

Cursor regression that started in 2.4.21 and has not been fixed as of 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 "ask the user" intent is dropped on the floor. The bridge reads cursor_version from stdin and downgrades to deny automatically because that's the safer default on both eras.

To opt out and let ask reach Cursor unchanged (it will still be silently mishandled, but the bridge stays out of the way):

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

could not detect host from stdin (scores: {...}); pass --host or { host } explicitly

Detection scored below the 0.5 threshold. install writes --host <host> into each generated config, so this usually only fires for hand-wired hooks. Pass --host in the host config or run(hooks, { host: "claude" }) programmatically.

handler for '<event>' threw: <message>

Your handler raised. Exit code 1. The host treats this as a non-blocking error.

'<host>' adapter failed to parse: <message>

Stdin had the right shape for the chosen host but a required field was missing or the wrong type.

License

MIT © Pavel Ivanov