Skip to content

Hooks in detail

bdrive init runs this for you — that is why setup is one command. This is what it does, for when you want to run it yourself, review what changed, or debug a folder that isn’t syncing.

Terminal window
bdrive hooks install # every detected platform, this machine
bdrive hooks install --agent claude,codex,gemini,hermes
bdrive hooks # status table
bdrive hooks uninstall # remove them again

Each platform gets the same three hooks written into its own user-level config, in its own format:

Platform Config it writes Pull / push / read events
Claude Code ~/.claude/settings.json UserPromptSubmit / PostToolUse (Write|Edit|MultiEdit) / PostToolUse (Read|Grep|Bash)
Codex ~/.codex/hooks.json UserPromptSubmit / PostToolUse (apply_patch) / PostToolUse (read_file|shell)
Gemini CLI ~/.gemini/settings.json BeforeAgent / AfterTool (write_file|replace|edit) / AfterTool (read tools)
Hermes ~/.hermes/config.yaml pre_llm_call / post_tool_call (write_file|patch) / post_tool_call (read_file|grep|bash)

Three hooks, three jobs:

  • Pull, before the agent answers, so it never reads a stale file. This one blocks — it is the only place BearDrive makes you wait, and it is why the whole thing works.
  • Push, after an edit, so teammates see the change within seconds even when no daemon is running.
  • Read tracking, on the agent’s read-shaped tools, queued locally and sent on the next sync. This is what fills the Dashboard. Listing tools are deliberately excluded: seeing a filename is not reading it.

When the sync daemon is running and up to date with the hub (it has pulled since the last change it was told about, and its connection to the hub is alive), the pull and push hooks leave the syncing to it: they only record the session and report what changed, and return in milliseconds (after waiting for a daemon sync already in progress to finish). Without a daemon, or when the daemon cannot vouch for the folder, they run a full sync themselves.

A session that starts above its synced folders (your home folder, or a repo root whose wiki/ is the synced folder) never runs that full sync: it records the session, reports what the daemon has pulled so far, and starts the daemon if it is not running. Walking every synced folder below your home folder on each prompt is slower than the hook’s timeout.

Every platform pipes hook JSON with a session id, so one hook command serves all four, and changes are stamped with <agent> session <id> — visible in bdrive log and the hub’s history.

Codex hooks are experimental and off by default. Turn them on in ~/.codex/config.toml:

[features]
codex_hooks = true

Codex then asks once to trust the hook definition. Answer yes.

Merging is idempotent and preserves hooks you already have. Each hook carries its own marker, so a config written before a hook existed gains just the missing one, and a registered hook’s matcher is upgraded in place when coverage grows.

Re-run bdrive hooks install after a CLI upgrade, once per machine.

Hooks are registered once per machine, in each platform’s own user config — never inside a project. Agent platforms read hook config only from the directory a session starts in: never a parent, never a subfolder. A file in the project would fire only for the sessions that happened to start exactly there, and — if the project is synced — would travel to the whole team. A user-level registration covers every session in every folder instead.

So BearDrive writes no agent-config directory into your project, and teammates don’t inherit your hooks: each device registers its own the first time it runs bdrive init. Earlier versions did write project-level hooks; installing strips those out as it goes, so nothing ends up running twice.

The hook opens with a shell guard that makes it a fast no-op in any folder without a .bdrive/ directory, which is what makes registering it globally safe.

bdrive hooks uninstall takes them back out — it removes only BearDrive’s own entries and leaves every other hook in those files untouched. Syncing itself is unaffected; only turn-boundary sync stops.

The sync daemon is an ordinary background process, so a restart ends it. bdrive init therefore also registers a login item that runs bdrive resume, which starts or wakes the global sync supervisor for every project this device syncs and has not paused:

Terminal window
bdrive autostart # is it registered?
bdrive autostart install # register it (init already did)
bdrive autostart uninstall # stop starting sync at login
bdrive resume # start or wake the sync daemon right now

One registration covers every project — add or remove projects freely, nothing to re-register. Projects paused with bdrive stop stay paused; only bdrive init resumes those.

Where it lives, per platform — user-level either way, no sudo, nothing machine-wide:

Platform What gets written
macOS ~/Library/LaunchAgents/ai.beardrive.daemon.plist (launchd loads it at login)
Linux ~/.config/systemd/user/beardrive.service plus the default.target.wants symlink that enables it (honors XDG_CONFIG_HOME)

Linux needs systemd as the init system. Without it — Alpine or another runit/OpenRC distro, WSL1, a slim container — bdrive autostart says so rather than writing a unit nothing would read.

On macOS, the moment that file is written you get a “Background Items Added” notification, and bdrive appears in System Settings → General → Login Items. That notice is macOS reporting the registration above — every way of starting at login triggers it, including Apple’s own SMAppService and a plain crontab — so bdrive init asks first on a terminal and says what is about to happen when it can’t ask. Answer no, or run bdrive autostart uninstall, and the item goes away; sync then resumes on the next bdrive resume, bdrive init, or agent turn instead of at login.

Either way this is not the only thing that recovers sync: an agent turn in a project syncs it too, so a machine you actually work on catches up on its own.