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
# 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 acmeOptions
| Flag | Default | Notes |
|---|---|---|
<script> (positional) | required | Path 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 events | CSV: SessionStart,PreToolUse,PostToolUse,UserPromptSubmit,Stop |
--hosts <list> | claude,cursor,codex | CSV 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-run | off | Print the would-be configs to stdout. Touches no files. |
--cwd <path> | process.cwd() | Configs go under <cwd>/.claude/, <cwd>/.cursor/, <cwd>/.codex/ |
--global, -g | off | Write to each host's user-level config (see Global mode). Overrides --cwd and --config-root. |
--config-root | off | Treat <cwd> as the host's config dir directly. See Config-root mode. |
--help, -h | Print 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.
# 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|WriteEach 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.
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts --hosts autoMirror 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:
bunx @pivanov/agent-hooks-bridge install ~/scripts/format.ts --globalMirror 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
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts \
--cwd ~/.claude-main --config-root --hosts claudeUse --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:
bunx @pivanov/agent-hooks-bridge install ./.hooks/scoped.ts -- --user alice --org acmeThis produces commands like:
./.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 acmeUse 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:
bunx @pivanov/agent-hooks-bridge install ./.hooks/scoped.ts -- --user '"Pavel Ivanov"' --org acmeAuto-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-bridgeand# <<< agent-hooks-bridgeis replaced as a whole. Content outside the markers is preserved verbatim.
Safe to:
- Re-run after editing your script.
- Re-run with a different
--eventsset; 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.
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" },
],
});| Option | Type | Notes |
|---|---|---|
argv | readonly string[] | Required. Same shape as process.argv.slice(2). -- separator works. |
targets | readonly IInstallTarget[] | Optional. If absent, a single target is built from --cwd, --hosts, and --config-root. When set, those CLI flags are ignored. |
fs | IInstallFs | Optional. Inject readFile / writeFile / mkdir for testing. |
log | (message: string) => void | Optional. Defaults to writing to process.stdout. |
IInstallTarget:
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
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-runRemoves every hook entry whose command matches the given script (with or without a trailing --host <id> and any extra arguments appended at install time).
| Flag | Default | Notes |
|---|---|---|
<script> (positional) | required | Path that was used at install time |
--hosts <list> | claude,cursor,codex | CSV of host ids, or auto to act only on hosts that look installed on this machine. |
--dry-run | off | Print the would-be configs to stdout. Touches no files. |
--cwd <path> | process.cwd() | Resolve <cwd>/.claude/, <cwd>/.cursor/, <cwd>/.codex/. |
--global, -g | off | Uninstall from each host's user-level config. Mirrors the same flag on install. |
--config-root | off | Treat <cwd> as the host's config dir directly. Mirrors the same flag on install. |
--help, -h | Print 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
hooksblock 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-bridgeand# <<< 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
--eventsflag; uninstall removes every entry pointing at the script regardless of which event it is wired to.
Generated paths
| Host | File | Format |
|---|---|---|
| Claude | .claude/settings.json | JSON |
| Cursor | .cursor/hooks.json | JSON |
| Codex | .codex/hooks.toml | TOML (managed block) |
See Generated Configs for the exact shapes.