# Agent hooks Hooks let you run your own commands at fixed points in the BitFun Agent's lifecycle: before and after a tool call, when a permission prompt would appear, when a prompt is submitted, around context compaction, around subagents, and when a session or turn starts or ends. A hook can observe what the Agent is doing, add context the model will read, rewrite a tool call's arguments, or block an action outright. ## BitFun hooks are Codex hooks BitFun implements **the Codex hook contract**, not a BitFun dialect: - the same `hooks.json` document — events, matcher groups, handler fields; - the same event names (`PreToolUse`, `PostToolUse`, `PermissionRequest`, `UserPromptSubmit`, `PreCompact`, `PostCompact`, `SessionStart`, `SessionEnd`, `SubagentStart`, `SubagentStop`, `Stop`); - the same JSON payload on stdin, with the same field names; - the same exit-code meanings (`0` success, `2` block with stderr as the reason, anything else a non-blocking error); - the same JSON decision schema on stdout (`permissionDecision`, `updatedInput`, `additionalContext`, `decision`/`reason`, …). **A Codex hook script runs in BitFun unchanged, and vice versa — there is nothing to port.** So this page does not restate the reference. For event semantics, the exact payload fields per event, and the decision schema, use Codex's own documentation, which covers all of it well: **→ ** The rest of this page is only what is BitFun-specific: where the files live, how to switch hooks on, and where BitFun currently differs. ## Where BitFun reads hooks Codex reads `~/.codex/hooks.json`; BitFun reads its own config directory instead. Everything inside the file is identical. | Scope | Path | | --- | --- | | User | `/config/hooks.json` | | Project | `/.bitfun/config/hooks.json` | The user config directory is `~/.config/bitfun` on Linux, `~/Library/Application Support/bitfun` on macOS, and `%APPDATA%\bitfun` on Windows. Both layers are additive: every matching handler runs, user handlers first. There is no override or shadowing between them. Changes are picked up without restarting BitFun. ## Turning hooks on **Settings → Agent Hooks**, or directly under the `app` section of `/config/app.json`: ```json { "app": { "hooks": { "enabled": true, "project_hooks_enabled": true } } } ``` | Setting | Default | Meaning | | --- | --- | --- | | `app.hooks.enabled` | `true` | Master switch. `false` disables all hooks. | | `app.hooks.project_hooks_enabled` | `false` | Whether the project hook file is honored. | **Project hooks are off by default.** A project hook file executes commands that live inside a checked-out repository, so anyone who can land a commit could otherwise run code on your machine. Turn it on only for repositories you trust, and re-check the file after pulling. Codex's `[features] hooks = false` has no BitFun equivalent — use `app.hooks.enabled` instead. ## Importing Claude Code and Codex command hooks BitFun can take a reviewed local snapshot of compatible `type: "command"` hooks discovered from Claude Code or Codex. This is an explicit copy, not a live mount of another product's configuration: 1. Open **Settings → Agent Hooks** or run `/hooks` in the TUI. Use `bitfun hooks list` from the root CLI for a scriptable view. 2. Choose a source and review every effective command, Windows override, timeout, copied or external dependency, skipped item, and the plan fingerprint. 3. Confirm that exact plan. If the source changed after review, BitFun writes nothing and returns a refreshed plan for another confirmation. User sources are copied to BitFun's user-managed data. Project sources are copied to workspace-isolated data under BitFun's project runtime area. Safe relative script dependencies beneath a source's `.claude/hooks` or `.codex/hooks` directory are copied into the immutable snapshot. Absolute dependencies remain external and are called out during review; moving or changing one can therefore change behavior without updating the snapshot. Dynamic paths, globs, escaping paths, links, unreadable files, and files beyond the fixed import limits are skipped rather than followed implicitly. An imported source can be enabled, disabled, updated, or removed independently. Removing it deletes only BitFun's managed copy; it never edits Claude Code or Codex files. Updates always require another exact-command review. Imported layers run in this fixed order: 1. manual user `hooks.json`; 2. enabled user imports, ordered by stable import id; 3. manual project `hooks.json`, when project hooks are enabled; 4. enabled project imports, ordered by stable import id. Import, update, enable, disable, and remove take effect on the next matching Hook event; an already running Hook finishes against the snapshot it started with. BitFun does not re-import on startup, poll, or watch Claude Code/Codex files. Use **Refresh** or `/hooks refresh` to check for source changes, then review an update explicitly. The management and execution paths are local-only; remote workspaces return unsupported instead of running local commands against a remote path. OpenCode plugin Hooks are intentionally excluded. Their JavaScript callbacks need the OpenCode plugin execution domain; the current OpenCode Hook catalog is still discovery/static preview and is not executable. Root CLI equivalents are: ```text bitfun hooks list [--refresh] [--format text|json] bitfun hooks import --source [--confirm ] bitfun hooks update [--confirm ] bitfun hooks enable bitfun hooks disable bitfun hooks remove --confirm bitfun hooks reset --confirm ``` Import and update are preview-only without the matching fingerprint. TUI uses the same backend and keeps `/hooks_external` and `/hooks-external` as aliases for the unified `/hooks` management view. `reset` is available only as explicit recovery for a corrupt BitFun-managed index and never changes source files. ## Quick start Create `/config/hooks.json`: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.command' >> ~/bitfun-commands.log" } ] } ] } } ``` Start a new session and ask the Agent to run a shell command; each command it runs is appended to `~/bitfun-commands.log`. A hook that blocks — here, refusing edits under `migrations/`: ```json { "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "python3 ~/hooks/protect.py" }] } ] } } ``` ```python #!/usr/bin/env python3 import json, sys payload = json.load(sys.stdin) if "/migrations/" in payload.get("tool_input", {}).get("file_path", ""): print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Migrations are generated; edit the schema instead.", } })) sys.exit(0) ``` ## Where BitFun differs from Codex Everything not listed here behaves as the Codex documentation describes. ### Not supported | Codex feature | BitFun | | --- | --- | | `config.toml` `[hooks]` table | not read — put hooks in `hooks.json` | | `[features] hooks = false` | use `app.hooks.enabled` | | Plugin-bundled and managed hooks (`PLUGIN_ROOT`, `managed_dir`) | not supported | | `prompt` and `agent` handler types | parsed so shared files stay valid, but skipped — only `type: "command"` executes | | Remote workspaces | hooks are skipped entirely: a local hook process and a remote workspace path do not describe the same filesystem | ### Fields not populated yet | Field or event | Current behavior | | --- | --- | | `transcript_path`, `agent_transcript_path` | always `null` | | `permission_mode` | only `default` or `bypassPermissions` | | `SessionStart.source` | only `startup`; `resume`, `clear`, `compact` are not dispatched | | `SessionEnd.reason` | always `other` | | `SubagentStop.stop_hook_active` | always `false` | | `SubagentStop` | dispatched when a subagent settles successfully, not on failure, cancellation, or timeout | | `Stop` | top-level turns only; subagent turns report through `SubagentStop` | ### Behavior worth knowing - **A hook can narrow the permission policy, never widen it.** A `PreToolUse` `permissionDecision: "allow"` waives the interactive prompt, but a tool call denied by a permission rule stays denied. - `suppressOutput` is parsed and currently ignored. - `continue: false` is honored for `PreToolUse` and `UserPromptSubmit`; for other events use `decision: "block"`. - `PostToolUse` fires for error results too, not only successes. - Limits: 1 MiB per `hooks.json`, 2048 handlers inspected across all layers (invalid and non-`command` handlers count toward it), and 10,000 bytes of model-visible text per hook before truncation. ## Security A hook is arbitrary code that runs with your user account's full privileges, every time its event fires. Treat `hooks.json` like a shell profile: - Review any hook you did not write before enabling it. - Keep project hooks off unless you trust everyone who can commit to the repository. - Payload values (prompts, tool arguments, file paths) are model- and user-supplied text. Parse them as JSON and never interpolate them into a shell command — that is why the examples above read fields with `jq`/`json.load`. - Do not print secrets to stdout for `SessionStart`, `UserPromptSubmit`, or `SubagentStart`, where plain stdout becomes context the model reads. ## Troubleshooting | Symptom | Cause | | --- | --- | | No hook runs at all | `app.hooks.enabled` is `false`, the file is not at the documented path, or the workspace is remote. | | Project hooks do not run | `app.hooks.project_hooks_enabled` is `false` (the default). | | The whole file is ignored | Invalid JSON, or a root key other than `description`/`hooks`. | | One event is ignored | Misspelled event name — the names are case-sensitive. | | A handler never runs | Its matcher does not match, or the matcher is not a valid pattern. Matchers are regular expressions anchored to the whole value, so `Bash` matches `Bash` but not `BashOutput`. | | A `prompt`/`agent` handler never runs | Only `type: "command"` handlers execute. | | Blocking has no effect | Blocking needs exit code 2 (reason on stderr), or a `decision`/`permissionDecision` field on stdout with exit code 0. | | Plain `echo` output is not visible to the model | Only `SessionStart`, `UserPromptSubmit`, and `SubagentStart` turn plain stdout into context; elsewhere use `hookSpecificOutput.additionalContext`. | Configuration problems, non-zero exits, timeouts, and hook decisions are written to the BitFun backend log. See [`src/crates/LOGGING.md`](../../src/crates/LOGGING.md) for how to raise the log level. ## Related - CLI `/hooks` shows manual and imported layers, discovers supported external sources asynchronously, and owns the import management actions described above. Edit `hooks.json` directly only for manual BitFun layers. - `/hooks_external` and `/hooks-external` are compatibility aliases for the same view; they do not create a second import or execution path.