Project files
Each synced folder carries its own settings, so configuration travels with the project.
.bdrive/
Section titled “.bdrive/”The folder’s settings directory. config.json holds the stable mount id
plus the project and remote (older mounts may also carry a legacy include
list — still honored, never written now).
{ "id": "m-5a10b713", "volume": "notes", "remote": "https://drive.example.com/p/7f3a2c91-4d5e-4b8a-9c17-2ad0f6b3e9c4" }Written by bdrive init and safe to hand-edit — a running daemon picks changes
up automatically. Which subfolders sync is not stored here: that lives in
.bdriveignore (see below), edited with bdrive scope add/rm. Mounts created
before that change may still carry an include list here; it is still honored,
but nothing writes it any more.
It is never synced and holds no credentials; the session token stays in
~/.bdrive.
Because everything is keyed by the mount id, the folder can be renamed or moved
freely. Copy it to another machine and bdrive init resumes the same project.
post_sync
Section titled “post_sync”An optional shell command run on this device after a sync applies changes from the hub — the event a local search index, cache or notifier can hang off instead of polling.
{ "id": "m-5a10b713", "volume": "notes", "remote": "https://drive.example.com/p/7f3a2c91-…", "post_sync": "qmd update && qmd embed" }The applied batch arrives as JSON on stdin, with the command’s working directory set to the folder:
{ "project": "m-5a10b713", "folder": "/Users/you/notes", "changed": [ { "path": "wiki/onboarding.md", "op": "write" }, { "path": "notes/retired.md", "op": "delete" } ] }- Once per cycle that applied at least one path — an initial sync of 400 files is one invocation, not 400.
- Inbound only. A cycle that only commits and pushes your own edits fires
nothing. (
bdrive restoredoes count: it writes an older version back into the folder.) - Never blocks sync. The command is spawned detached; one that hangs, exits
non-zero or does not exist is logged to
daemon.logand forgotten. - Off unless set. No
post_synckey, no new behavior.
This is local configuration and nothing else can set it: .bdrive/ never syncs,
so no hub response and no teammate’s change can put a command on your machine.
The one path in is your own hands — .bdrive/ travels with a folder, so a
folder you copy from a teammate brings their post_sync along with it.
.bdriveignore
Section titled “.bdriveignore”A gitignore-style opt-out list at the mount root. It syncs like a normal file, so every device shares the same rules. See Scoping the folder.
Paths BearDrive never carries
Section titled “Paths BearDrive never carries”Some paths are excluded in both directions — never scanned, never uploaded,
never written onto a teammate’s disk — regardless of .bdriveignore:
| Path | Why |
|---|---|
.bdrive/ |
The mount’s own identity. Syncing it would let one device repoint another. |
.git/ |
Carries hook scripts that would run on a teammate’s next commit. |
.claude/settings.json, .claude/settings.local.json, .codex/hooks.json, .codex/config.toml, .gemini/settings.json, .hermes/config.yaml |
Agent hook configuration is a shell command a teammate would be installing on your machine. BearDrive’s own hooks go in each machine’s user config instead. |
.mcp.json |
Project-scoped MCP servers: command + args pairs your agent LAUNCHES when a session starts in the folder. Same reason as the hooks above. |
.DS_Store, .bdrive-tmp-* |
Noise and in-flight temp files. |
Everything else under an agent-config directory — .claude/skills,
.claude/commands, .claude/agents, AGENTS.md, CLAUDE.md — syncs
normally. Sharing what an agent reads is the product; sharing what it runs
is not. See What agents read. An agent connected
over MCP has no folder to load
AGENTS.md / CLAUDE.md from, so the hub’s tools show it to them instead.
.bdrive/workspace.json — a workspace root
Section titled “.bdrive/workspace.json — a workspace root”A folder that holds projects, rather than being one, can carry a manifest naming which of its immediate children are project folders — one this device has paused, or has never connected, is listed too: the manifest answers “which of these are BearDrive projects”, not “which are syncing right now”. The Mac app writes one at the folder you connect; the CLI never creates one on its own.
// <root>/.bdrive/workspace.json{ "kind": "workspace", "projects": [ { "path": "team", "id": "m-5a10b713" } ] }A root is not a project: nothing in it syncs — not the manifest, not the
folders beside your projects — and bdrive init refuses to mount one, telling
you to run it in a folder underneath instead. A root is never a project folder
itself. (Roots are not meant to nest either, but connecting two folders where
one is inside the other will produce that — harmlessly: the inner folder simply
ends up with two indexes, and an index decides nothing.)
The file is only an index — identity lives in each project’s own config.json
— so a wrong or stale entry is harmless: every daemon start rebuilds the
list from what is actually on disk, and a hand-edited entry is corrected rather
than obeyed. Deleting the file is different: it is how you un-root the
folder. Nothing recreates it, and bdrive init will then treat the folder like
any other. A corrupted manifest has the same effect.
Run bdrive stop in the projects under it first, though: a
re-index that is already running can write the file back one last time, because
it decided the folder was a root before you deleted it.
And nothing else
Section titled “And nothing else”Inside a project, those two are all BearDrive puts there: .bdrive/config.json,
.bdriveignore, and your own files. (A project created from a template also
starts with an AGENTS.md and a directory skeleton — but those are ordinary
synced files, yours to edit or delete like any other; nothing reads them but
your agents.) No agent-config directory is ever created
here — the sync hooks live in each platform’s user config
(~/.claude/settings.json, ~/.codex/hooks.json, ~/.gemini/settings.json,
~/.hermes/config.yaml), written once per machine. See
Hooks in detail.
Global state
Section titled “Global state”Everything else lives under $BDRIVE_HOME (default ~/.bdrive):
| Path | Contents |
|---|---|
device.json |
This device’s identity, used in change tracking |
settings.json |
Default server, device token, signed-in account |
mounts.json |
Mount registry, keyed by stable mount id, holding each mount’s last-known path and shared volume_id when multiple worktrees connect to the same project |
daemon.lock |
Global supervisor liveness lock, held by the single bdrive daemon run process |
daemon.pid |
Global supervisor PID for display; liveness comes from the lock |
daemon.log |
Global supervisor log |
volumes/<volume-id>/ |
Shared local volume store: blobs, journals, sync state, and per-mount files |
Nothing is keyed by folder path, which is why moves and renames are free.
ResolveMount self-heals the registry path on the next command.
The volume store
Section titled “The volume store”~/.bdrive/volumes/<volume-id>/├─ blobs/ content-addressed file content (sha256)├─ journal/ one append-only op log per device├─ state-<mount-id>.json per-mount materialization cache├─ inbound-<mount-id>.jsonl per-mount inbound spool├─ paused-<mount-id> per-mount pause marker├─ sync.json lamport clock + push cursor└─ secrets-<mount-id>.json files that looked like they held credentials when they last changed (what `bdrive status` reports)Each active volume worker also holds daemon.lock in this directory and
writes daemon.pid and daemon.log here. The supervisor holds a separate
daemon.lock at the top of $BDRIVE_HOME, with its own PID and log files.
This Dual-Lock scheme allows one supervisor process per machine/user session
and one worker goroutine per active project volume. PID files alone never
prove that either is running.
bdrive stop pauses the selected mount. Once no active sibling worktrees
remain, the worker releases the volume lock without stopping other projects.
The global daemon exits cleanly when no active volumes remain.
bdrive init, bdrive resume, bdrive sync, and agent sync hooks start or
wake it for unpaused projects and replace it automatically after a binary
upgrade.
The hub’s storage adds two key classes the local store never holds: files
larger than 4 MiB travel as content-defined chunks/<sha256> pieces plus a
manifests/<sha256> chunk list keyed by the whole file’s hash (delta sync —
a small edit to a large file uploads roughly one chunk, not the file). Local
blobs stay whole; chunking exists only on the wire and in the hub’s store,
and the hub reassembles a whole blob on demand for any client that asks for
blobs/<sha256>, so older clients keep working unchanged.