Skip to content

install and uninstall

bunx @pivanov/agent-hooks-bridge install <script> [options] [-- <extra-args>...]
bunx @pivanov/agent-hooks-bridge uninstall <script> [options]

One command per direction, three host configs each. Both are idempotent. Install replaces only the entries pointing at your script; uninstall removes only those entries.

Usage

bash
# minimal: wires all 5 unified events into all 3 hosts
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts

# specific events only
bunx @pivanov/agent-hooks-bridge install ./.hooks/guard.ts --events PreToolUse,UserPromptSubmit

# specific hosts only
bunx @pivanov/agent-hooks-bridge install ./.hooks/dev.ts --hosts claude,codex

# tool matcher (PreToolUse / PostToolUse only)
bunx @pivanov/agent-hooks-bridge install ./.hooks/bash-guard.ts --events PreToolUse --matcher Bash

# different matcher per event
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts \
  --matcher PreToolUse:Bash,PostToolUse:Edit\|Write

# preview without writing
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts --dry-run

# install into a different directory
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts --cwd /path/to/project

# only write to hosts that look installed on this machine
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts --hosts auto

# write to user-level config (~/.claude/settings.json, ~/.cursor/hooks.json, $CODEX_HOME/hooks.toml)
bunx @pivanov/agent-hooks-bridge install ~/scripts/format.ts --global

# pass scoped flags to the hook (everything after '--' is appended to every command)
bunx @pivanov/agent-hooks-bridge install ./.hooks/scoped.ts -- --user alice --org acme

Options

FlagDefaultNotes
<script> (positional)requiredPath to your hook script. Stored verbatim when it has no whitespace; auto-quoted ("...") in the generated config when the path contains spaces.
--events <list>all 5 eventsCSV: SessionStart,PreToolUse,PostToolUse,UserPromptSubmit,Stop
--hosts <list>claude,cursor,codexCSV of host ids, or the literal auto to write only to hosts that look installed on this machine (probes ~/.claude, ~/.cursor, ~/.codex).
--matcher <pattern>*Tool matcher for PreToolUse/PostToolUse. Ignored on other events. Per-event syntax also accepted (see below).
--dry-runoffPrint the would-be configs to stdout. Touches no files.
--cwd <path>process.cwd()Configs go under <cwd>/.claude/, <cwd>/.cursor/, <cwd>/.codex/
--global, -goffWrite to each host's user-level config (see Global mode). Overrides --cwd and --config-root.
--config-rootoffTreat <cwd> as the host's config dir directly. See Config-root mode.
--help, -hPrint usage
-- <args>...Everything after a literal -- is appended to every generated command, after <script> --host <host>.

Per-event matcher

--matcher accepts either a single pattern (applied to every permission-like event) or a per-event map. Detection is automatic: any matcher containing : is parsed as the per-event form.

bash
# single pattern (legacy form; applies to PreToolUse and PostToolUse)
--matcher Bash

# per-event: PreToolUse matches Bash, PostToolUse matches Edit or Write
--matcher PreToolUse:Bash,PostToolUse:Edit|Write

Each segment must be <EventName>:<pattern>. Unknown event names and empty patterns are rejected at parse time. Events not listed in the map fall back to * (matches every tool). Non-permission events (SessionStart, UserPromptSubmit, Stop) ignore the matcher regardless of form.

Tip: the | character is special in most shells. Quote the matcher value or escape the pipe (Edit\|Write) when using zsh/bash.

Auto-detect mode (--hosts auto)

Pass --hosts auto to write only to hosts whose config directory exists on the current machine. The detector probes:

  • Claude: ~/.claude/ or ~/.claude.json
  • Cursor: ~/.cursor/
  • Codex: $CODEX_HOME (defaults to ~/.codex/)

If you only have one or two of the three installed, auto skips the others rather than littering your project with config files no host will read. The flag throws if zero hosts are detected, so you don't silently install nothing.

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

Mirror on uninstall and doctor.

Global mode (--global)

--global (alias -g) writes to each host's user-level config instead of a project-local one:

  • Claude: ~/.claude/settings.json
  • Cursor: ~/.cursor/hooks.json
  • Codex: $CODEX_HOME/hooks.toml (default ~/.codex/hooks.toml)

--global overrides --cwd and --config-root. Use it when you want a hook to fire across every project on your machine without per-project setup:

bash
bunx @pivanov/agent-hooks-bridge install ~/scripts/format.ts --global

Mirror on uninstall and doctor. Programmatic equivalent: targets: [{ cwd: "ignored", global: true }].

Config-root mode

--config-root is the escape hatch for unusual layouts (e.g. side-by-side Claude installs in ~/.claude-main, ~/.claude-ship). It treats <cwd> as the host's config dir directly and drops the leading subdirectory:

  • Claude: <cwd>/settings.json
  • Cursor: <cwd>/hooks.json
  • Codex: <cwd>/hooks.toml
bash
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts \
  --cwd ~/.claude-main --config-root --hosts claude

Use --global for the standard user-level install; reach for --config-root only when the path you want isn't ~/.claude, ~/.cursor, or $CODEX_HOME.

Mirror flag on uninstall. Programmatic equivalent: targets: [{ cwd: "~/.claude-main", configRoot: true }].

Resolution order: global > configRoot > project (cwd + per-host project path). For Codex global paths, $CODEX_HOME is consulted (defaults to ~/.codex/).

Extra arguments

Hosts spawn hook scripts in fresh processes; environment variables don't reliably propagate. The bridge writes <script> --host <host> into each config; anything you put after -- on the install line is appended verbatim:

bash
bunx @pivanov/agent-hooks-bridge install ./.hooks/scoped.ts -- --user alice --org acme

This produces commands like:

text
./.hooks/scoped.ts --host claude --user alice --org acme
./.hooks/scoped.ts --host cursor --user alice --org acme
./.hooks/scoped.ts --host codex --user alice --org acme

Use this to pass per-environment scope (user, org, project) to your hook. Uninstall matches by prefix on <script> (with the trailing space), so changing --user alice to --user bob later still removes the previous entry.

Extra-args are not auto-quoted. If a value contains whitespace, quote it in the install invocation so the inner quotes survive into the generated config command:

bash
bunx @pivanov/agent-hooks-bridge install ./.hooks/scoped.ts -- --user '"Pavel Ivanov"' --org acme

Auto-quoting only kicks in for the script path itself.

Idempotency

Running install twice produces the same config files. Entries are matched by their command value: any entry whose command is <your script> or starts with <your script> is treated as managed and replaced. The matcher also recognizes "<your script>" and '<your script>' so re-installs of paths with whitespace stay idempotent and hand-quoted entries are still detected.

  • Claude and Cursor (JSON): managed entries are removed before fresh ones are added. Other entries are left in place.
  • Codex (TOML): the block between # >>> agent-hooks-bridge and # <<< agent-hooks-bridge is replaced as a whole. Content outside the markers is preserved verbatim.

Safe to:

  • Re-run after editing your script.
  • Re-run with a different --events set; stale event entries for the same script are removed.
  • Run once per script when you have multiple managed scripts; each tracks its own entries.

Out of scope

  • Codex TOML is not fully parsed. Hand-written [[hooks]] entries outside the marker block are preserved by string operations only.
  • The script's existence, executable bit, and shebang are not validated.
  • Runtime dependencies (Bun, Node, biome, etc.) are not installed.

Programmatic API

runInstall and runUninstall are exported from the package root for programmatic use. The CLI is a thin wrapper around them.

ts
import { runInstall, runUninstall } from "@pivanov/agent-hooks-bridge";
import type { IInstallTarget } from "@pivanov/agent-hooks-bridge";

await runInstall({
  argv: ["./.hooks/format.ts", "--", "--user", "alice"],
  // optional: write the same config into multiple cwds in one call
  targets: [
    { cwd: "/home/x/.claude" },
    { cwd: "/home/x/.claude-main" },
    { cwd: "/home/x/.claude-ship", hosts: ["claude"] }, // per-target host filter
  ],
});

await runUninstall({
  argv: ["./.hooks/format.ts"],
  targets: [
    { cwd: "/home/x/.claude" },
    { cwd: "/home/x/.claude-main" },
    { cwd: "/home/x/.claude-ship" },
  ],
});
OptionTypeNotes
argvreadonly string[]Required. Same shape as process.argv.slice(2). -- separator works.
targetsreadonly IInstallTarget[]Optional. If absent, a single target is built from --cwd, --hosts, and --config-root. When set, those CLI flags are ignored.
fsIInstallFsOptional. Inject readFile / writeFile / mkdir for testing.
log(message: string) => voidOptional. Defaults to writing to process.stdout.

IInstallTarget:

ts
interface IInstallTarget {
  cwd: string;
  hosts?: readonly ("claude" | "cursor" | "codex")[];
  configRoot?: boolean;
  global?: boolean;
}

hosts overrides the per-call --hosts if provided. configRoot switches that target to config-dir path mode. global writes to each host's user-level config (see Global mode) and overrides both cwd and configRoot.

The CLI surface stays single-target (--cwd <path>); multi-target is programmatic only.

uninstall

bash
bunx @pivanov/agent-hooks-bridge uninstall ./.hooks/format.ts
bunx @pivanov/agent-hooks-bridge uninstall ./.hooks/format.ts --hosts claude
bunx @pivanov/agent-hooks-bridge uninstall ./.hooks/format.ts --dry-run

Removes every hook entry whose command matches the given script (with or without a trailing --host <id> and any extra arguments appended at install time).

FlagDefaultNotes
<script> (positional)requiredPath that was used at install time
--hosts <list>claude,cursor,codexCSV of host ids, or auto to act only on hosts that look installed on this machine.
--dry-runoffPrint the would-be configs to stdout. Touches no files.
--cwd <path>process.cwd()Resolve <cwd>/.claude/, <cwd>/.cursor/, <cwd>/.codex/.
--global, -goffUninstall from each host's user-level config. Mirrors the same flag on install.
--config-rootoffTreat <cwd> as the host's config dir directly. Mirrors the same flag on install.
--help, -hPrint usage

Behavior:

  • If a config file does not exist, that host is skipped silently.
  • If a config file exists but contains no managed entries, the file is left untouched and the runner prints skipped <path> (no managed entries).
  • Claude and Cursor: empty event arrays are removed; the hooks block is dropped entirely if every event becomes empty. Other top-level keys (permissions, version, etc.) are preserved.
  • Codex: the entire managed block (between # >>> agent-hooks-bridge and # <<< agent-hooks-bridge) is removed. Content outside the markers is preserved verbatim. If the file becomes empty, it is overwritten with an empty string (the file itself is not deleted).
  • There is no --events flag; uninstall removes every entry pointing at the script regardless of which event it is wired to.

Generated paths

HostFileFormat
Claude.claude/settings.jsonJSON
Cursor.cursor/hooks.jsonJSON
Codex.codex/hooks.tomlTOML (managed block)

See Generated Configs for the exact shapes.

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