mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
An audit of every doc surface against the code turned up claims that the user-scope hook move and the one-command init made false: project-level hooks "riding the repo", the Claude trust prompt, Codex's //hooks project layer, `--no-hooks` skipping the skill (it does not), prune reconciling against a per-device scope (it now refuses on a scoped project), and `--scan-interval`/`--remote-interval` documented as init flags when they only exist on `bdrive daemon run`. Also documents the surface added today — `--server`, `bdrive hooks uninstall`, and the plugin's PreToolUse auto-approval — refreshes the two sample `init` transcripts to the real output, and corrects hook matchers that had drifted from agenthooks.go. `bdrive scope` told users to narrow an existing mount with `bdrive init . --only <dirs>`, which resume then ignored — a dead end. Init now applies --only on resume, writing the scope block, so the advice works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016aYntCWwdUhpzUfEk3ddyJ
613 lines
32 KiB
Markdown
613 lines
32 KiB
Markdown
# BearDrive — Google Drive for AI agents
|
||
|
||
**BearDrive** mounts any folder as a synced volume: its contents stay
|
||
synchronized across all your devices and teammates through a BearDrive
|
||
**hub**, every change is tracked (who, when, on which device), and
|
||
everything keeps working offline. The CLI is `bdrive`; a hub is a
|
||
`bdrive web` server you (or we) run on an object store — clients sync
|
||
through it over HTTPS and never touch the storage directly.
|
||
|
||
What it's for, first and foremost: **sharing context across AI agents** —
|
||
give every agent on the team the same folder as memory, and your agent
|
||
knows what their agent knows. (People are covered too: any synced file
|
||
becomes a public URL that renders as a page.) Notes, plans, findings, and
|
||
artifacts follow the team everywhere — and unlike a memory API, they stay
|
||
**real files with provenance**: every change is attributed to the human,
|
||
agent, and device that made it, and the hub's Dashboard shows what your
|
||
agents actually read (and which hot-but-stale docs nobody maintains).
|
||
|
||
<p align="center">
|
||
<img src="docs/assets/insights.png" alt="Knowledge Insights — every file plotted by agent/human reads vs staleness; hot-but-stale docs are the danger zone" width="820">
|
||
</p>
|
||
|
||
| Browse with read heat | Public share pages |
|
||
|---|---|
|
||
|  |  |
|
||
|
||
```console
|
||
$ bdrive login https://your-hub # once per device — self-host a hub in ~10 min (docs/self-hosting.md)
|
||
$ cd ~/workspace && bdrive init
|
||
initialized /Users/snow/workspace
|
||
server: https://your-hub
|
||
project: workspace (p-7f3a2c91)
|
||
skill: installed for claude, codex
|
||
claude hooks registered → /Users/snow/.claude/settings.json
|
||
daemon: running (pid 55434, scan 3s, remote sync 10s)
|
||
```
|
||
|
||
> BearDrive Cloud — zero-setup, bare `bdrive login`, free personal
|
||
> workspace on signup — at [beardrive.ai](https://beardrive.ai). Or
|
||
> self-host your own hub.
|
||
|
||
On another machine:
|
||
|
||
```console
|
||
$ bdrive login https://your-hub && cd ~/workspace && bdrive init
|
||
# … connect the same project; the files appear and stay in sync
|
||
```
|
||
|
||
## Features
|
||
|
||
- **Any folder is a project** — `bdrive init` turns any folder into a synced
|
||
project. Files are *real files on disk*: every tool, editor, and agent can
|
||
use them with zero integration work. Rename or move the folder freely —
|
||
state is keyed by a stable id, never the path.
|
||
- **Multi-device sync** — devices converge through a shared hub. Each
|
||
device only writes its own append-only journal, so no locking service is
|
||
needed; the hub can be backed by any object store.
|
||
- **Change tracking** — `bdrive log` and the web UI's History view show
|
||
which account changed which file, when, from which device (name, OS, IP).
|
||
Content is stored content-addressed, so every version is retained — view
|
||
or download any point in a file's history.
|
||
- **Cloud-provider agnostic** — a hub can store on Amazon S3 (`s3://`),
|
||
Google Cloud Storage (`gs://`), any S3-compatible store (MinIO, Cloudflare
|
||
R2 via `AWS_ENDPOINT_URL`), or a plain shared directory (`file://`, e.g. a
|
||
NAS). Clients never see it.
|
||
- **Offline-first** — the working folder is always fully usable with no
|
||
network. Changes are journaled locally and pushed when the remote becomes
|
||
reachable again.
|
||
- **Conflict-safe** — concurrent edits resolve deterministically
|
||
(last-writer-wins), and the losing version is preserved as a
|
||
`name.bdrive-conflict-<device>-<time>` file. Nothing is silently dropped.
|
||
- **Selective sync** — a gitignore-style `.bdriveignore` opts files out, and
|
||
`bdrive init . --only wiki,docs` (or the interactive prompt) narrows a mount
|
||
to some of its subfolders by writing those same rules for you.
|
||
- **macOS & Linux.**
|
||
|
||
## Install
|
||
|
||
```sh
|
||
brew install runbear-io/tap/beardrive # macOS (and Linuxbrew); installs the `bdrive` CLI
|
||
```
|
||
|
||
or from source:
|
||
|
||
```sh
|
||
go install github.com/runbear-io/beardrive/cmd/bdrive@latest
|
||
```
|
||
|
||
## Quick start
|
||
|
||
```sh
|
||
# 1. Sign this device in against your hub (once per device).
|
||
# Self-host a hub in ~10 minutes (docs/self-hosting.md), then:
|
||
bdrive login
|
||
# (BearDrive Cloud: sign up in the browser, get a free personal
|
||
# workspace automatically. Self-hosting? bdrive login https://your-hub)
|
||
|
||
# 2. Start syncing a project — interactive: create or connect a project,
|
||
# sync the whole folder or just ./shared. Re-run any time to resume.
|
||
cd ~/my-project && bdrive init
|
||
|
||
# 3. Work normally — create, edit, delete files with any tool.
|
||
echo "remember this" > memory.md
|
||
|
||
# On every other device: `bdrive login https://your-hub` once, then bdrive init in a folder
|
||
# and connect the same project.
|
||
|
||
# See what changed, who changed it, and from which device
|
||
bdrive log
|
||
|
||
# Check sync state and the daemon
|
||
bdrive status
|
||
|
||
# Stop syncing — pauses everything, including agent turn hooks
|
||
# (files stay on disk; bdrive init resumes any time)
|
||
bdrive stop
|
||
```
|
||
|
||
Renaming or moving a project folder is safe: state is keyed by a stable
|
||
project id, never the path. The daemon notices the move, steps aside, and
|
||
the next `bdrive init` (or any bdrive command) at the new location resumes
|
||
exactly where it left off — zero re-scan, zero spurious changes.
|
||
|
||
### Credentials
|
||
|
||
beardrive uses each provider's standard credential chain — nothing beardrive-specific.
|
||
Note: **client devices always use an `https://` hub remote** — the
|
||
`s3`/`gs`/`file` rows below are how the *hub operator* configures the
|
||
hub's own storage, never something a syncing client points at directly:
|
||
|
||
| Remote | Credentials |
|
||
|---|---|
|
||
| `s3://bucket/prefix` | `AWS_PROFILE`, `~/.aws/credentials`, env vars, IAM roles. S3-compatible stores via `AWS_ENDPOINT_URL`. |
|
||
| `gs://bucket/prefix` | Application Default Credentials (`gcloud auth application-default login`) or `GOOGLE_APPLICATION_CREDENTIALS`. |
|
||
| `file:///path` | none — any local or network-mounted directory |
|
||
| `https://host:port/p/<id>` | none — syncs through a bdrive web hub; only the server holds storage credentials (see [The sync hub and `bdrive init`](#the-sync-hub-and-bdrive-init)) |
|
||
|
||
## Commands
|
||
|
||
| Command | Description |
|
||
|---|---|
|
||
| `bdrive login [server-url]` | Sign this device in (browser flow; `--device` forces the code flow, and shells without a TTY fall back to it automatically; default server beardrive.ai — the managed cloud, free personal workspace on signup; pass your hub URL to self-host). Switch hubs with `bdrive login <new-url>` |
|
||
| `bdrive logout` | Sign this device out — clear the saved token/account (`--forget` also drops the remembered server) |
|
||
| `bdrive init [folder]` | Create/connect a project and start syncing — the mount is always exactly the folder named. Interactive on a TTY, flags (`--name/--project/--server/--only/--yes`) for scripts; installs the agent skill, registers agent sync hooks in each platform's user config (`--no-hooks` skips the hooks), prints the project link; re-run to resume |
|
||
| `bdrive stop [folder]` | Stop syncing, including agent sync hooks (files stay; `bdrive init` resumes) |
|
||
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync — edits the managed block of `.bdriveignore` rules that `init --only` writes, so no one hand-writes negation syntax. The daemon picks changes up in seconds; `rm` deletes nothing, locally or on the hub |
|
||
| `bdrive forget <path>...` | Stop syncing a path *and* remove it from the hub — adds the rule to `.bdriveignore` (which syncs) and prunes in one step. Local files are never touched, here or on teammates' devices |
|
||
| `bdrive url [path]` | Internal hub link for a file/folder (sign-in + membership required; `--sync` pushes first; no arg = project home). Computed locally |
|
||
| `bdrive share <file>` | Public URL for a synced file (`--list`, `--revoke`, `--expires`) |
|
||
| `bdrive sync [folder]` | Run one sync cycle now. `--note <text>` stamps session context (e.g. an agent session id) onto changes — shown in `bdrive log` and hub history; keeps applying to daemon-committed changes until `--note-ttl` (default 30m) expires. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: event JSON on stdin, sync + note, gated-link formula (Claude Code hook JSON) on stdout |
|
||
| `bdrive hooks [install\|uninstall]` | Register turn-boundary sync hooks in each agent platform's user config (Claude Code, Codex, Gemini CLI, Hermes) — pull each turn, push after edits, session-note stamping, agent-read tracking. Once per machine, covering every session; run automatically by `bdrive init`; idempotent (`--agent` overrides detection) |
|
||
| `bdrive skill [install]` | Install the `beardrive` skill into detected agent platforms (`~/.codex/skills/beardrive/SKILL.md` and friends) so the agent can do the setup itself — sign in, `bdrive init`, and register the sync hooks; idempotent (`--agent` overrides detection) |
|
||
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads from a hook event (JSON on stdin) for the hub's read heatmap — native reads, grep matches, and files named in shell commands; drained on the next sync. Registered by `bdrive hooks install` |
|
||
| `bdrive status [folder]` | Projects, daemon state, pending changes |
|
||
| `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file |
|
||
| `bdrive export [folder]` | Export the whole project — every device's journal, all blobs, full history — from its hub to a portable `.tar.gz` (`-o` names the file) |
|
||
| `bdrive import <archive>` | Import an export archive as a new project on the hub you're logged into (`--name` overrides); history and authorship carry over. Move projects between hubs — cloud → self-hosted or back — with `export` + `login` + `import` |
|
||
| `bdrive web [folder \| storage-root-url]` | Web server: viewer (rendered markdown, downloads, history), uploads, multi-project sync hub |
|
||
| `bdrive whoami` | Signed-in account and device identity used in change tracking |
|
||
| `bdrive version` | Print the version (also `bdrive --version`) |
|
||
|
||
## Project files
|
||
|
||
Each mounted folder carries its own settings, so configuration travels with
|
||
the project:
|
||
|
||
- **`.bdrive/`** — the folder's settings directory: `config.json` holds the
|
||
**stable mount id** plus the project and remote (and, on older mounts, a
|
||
legacy `include` list — still honored, never written now). Written by
|
||
`bdrive init`, safe to hand-edit (a running daemon picks changes up
|
||
automatically). Never synced, and it 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.
|
||
- **`.bdriveignore`** — gitignore-style opt-out list at the mount root. Syncs
|
||
like a normal file, so every device shares the same rules. Supports `#`
|
||
comments, `*`, `**`, `?`, trailing `/` for directories, leading `/` (or any
|
||
`/`) for root-anchoring, and `!` to re-include.
|
||
|
||
```jsonc
|
||
// .bdrive/config.json
|
||
{ "id": "m-5a10b713", "volume": "notes",
|
||
"remote": "https://drive.example.com/p/p-7f3a2c91" }
|
||
```
|
||
|
||
Opting out is non-destructive: when a pattern starts matching an
|
||
already-synced file, the file stops syncing but is deleted nowhere — which
|
||
also means the hub keeps the copy it already has. `bdrive forget <path>` (or
|
||
`bdrive sync --prune` for rules you added by hand) takes it off the hub, and
|
||
still deletes nothing on disk: every device receives the rule alongside the
|
||
removal and simply stops tracking the path.
|
||
|
||
## Web server
|
||
|
||
`bdrive web` serves a website — browse folders and files, read markdown
|
||
rendered Obsidian-style (including `[[wikilinks]]`, task lists, and
|
||
tables), download any file — and, pointed at a storage root, becomes a
|
||
**multi-project sync hub**. It is read-only unless started with `--upload`.
|
||
|
||
```sh
|
||
bdrive web # serve the current directory (viewer)
|
||
bdrive web ./notes # serve a folder from disk (viewer)
|
||
bdrive web -c config.json # everything from a config file
|
||
bdrive web s3://my-bucket/root --upload # multi-project sync hub
|
||
```
|
||
|
||
With a folder it serves files straight from disk — on a BearDrive mount the
|
||
daemon keeps them fresh, so this is the simplest read-only deployment (no
|
||
cloud credentials on the serving machine). With a storage root URL it runs
|
||
in hub mode, described below.
|
||
|
||
Flags: `--addr` (default `:4173`), `--volume` (display name), `--refresh`
|
||
(listing cache, default `10s`), `--dir` / `--remote` (explicit forms of
|
||
the positional argument), `--upload` (allow client writes, off by default),
|
||
`--upload-ttl` (presigned-URL lifetime, default `15m`), `--projects-db`
|
||
(hub project registry file, default `$BDRIVE_HOME/projects.json`),
|
||
`-c/--config` (read all of the above from a JSON file; explicit flags win):
|
||
|
||
```jsonc
|
||
// bdrive web -c config.json
|
||
{
|
||
"remote": "s3://my-bucket/root", // storage root (hub) — or "dir": "./folder" (viewer)
|
||
"addr": ":4173",
|
||
"upload": true,
|
||
"upload_ttl": "15m",
|
||
"refresh": "10s",
|
||
"projects_db": "/var/lib/bdrive/projects.json",
|
||
"share_rpm": 120, // per-IP rate limit on public /s/* links
|
||
"auth": { // optional knobs; hub auth is always on
|
||
// Signup is invite-only by default. To allow self-service signup,
|
||
// open it WITH a gate (an ungated open hub is refused at startup):
|
||
"allow_signup": true,
|
||
"allowed_domains": ["example.com"], // only these domains may sign up
|
||
"require_approval": true, // …and an admin must approve each one
|
||
"users_db": "/var/lib/bdrive/auth.json",
|
||
"admins": ["admin@example.com"],
|
||
"smtp": { "host": "smtp.example.com", "port": 587,
|
||
"user": "drive@example.com", "pass": "…", "from": "drive@example.com" }
|
||
},
|
||
"reads": { // read heatmap telemetry (hub mode)
|
||
"enabled": true, // default true; aggregate counts only
|
||
"retention_days": 400 // daily buckets older than this fold into all-time totals
|
||
}
|
||
}
|
||
```
|
||
|
||
### The sync hub and `bdrive init`
|
||
|
||
In hub mode the server hosts many **projects** on one storage root — each
|
||
project's data lives under its own prefix (`<root>/<project-id>/`), and a
|
||
file-backed registry (`projects.json`, loaded at start, rewritten
|
||
atomically on every change) maps project ids to names. Client devices sync
|
||
whole folders through the hub without ever knowing where the storage is or
|
||
holding any cloud credentials; the server device is the only one configured
|
||
with the bucket.
|
||
|
||
Projects are walled by **organization**: every project belongs to one org
|
||
(file-backed `orgs.json`), and only that org's members — accounts with the
|
||
`owner` or `member` role — can see, browse, or sync it. Your first
|
||
`bdrive init` creates an org for you automatically; an owner invites
|
||
teammates from the web UI (the org name in the sidebar footer — Invite
|
||
mints an expiring join link, `/join/<token>`, that any signed-in account
|
||
can open to become a member). A hub upgraded from an earlier version
|
||
sweeps its existing projects into a `default` org that all existing
|
||
accounts join, so nothing breaks. Public share links stay outside the
|
||
wall on purpose.
|
||
|
||
Inside an org, each project carries its own **permissions** — four ordered
|
||
levels, edited under Project settings → People:
|
||
|
||
| Level | Can |
|
||
|---|---|
|
||
| `none` | nothing: the project is hidden — absent from the project list, every route denied |
|
||
| `read` | browse, view, download, history, read heat — and **pull**, so a device stays current |
|
||
| `write` | + upload, sync push, and minting/revoking share links |
|
||
| `admin` | + rename, delete, and edit this project's permissions |
|
||
|
||
The default is `write` for every org member, which is exactly the old
|
||
behavior — an upgraded hub changes nothing until someone edits
|
||
permissions. Setting the **default** to `No access` makes a project
|
||
invite-only: only explicit grants get in. Whoever creates a project becomes
|
||
its first admin, and **org owners are implicitly admin on every project in
|
||
their org**, so nobody can lock them out. Grants are org members only, and
|
||
a project always keeps at least one admin.
|
||
|
||
Two things follow on the **device** side, because a refusal is not the same
|
||
as being offline (see `bdrive status`):
|
||
|
||
- **read-only** — pushes are refused, so the daemon goes **pull-only**. Your
|
||
local edits stay journaled on the device, never pushed and never lost;
|
||
they go out if you're granted `write` again.
|
||
- **no access** — pulls are refused too, so sync **pauses**. Nothing is
|
||
pulled, pushed, or written: revoking access never deletes or reverts a
|
||
file on someone's disk. Re-granting resumes on the next tick.
|
||
|
||
Public `/s/<token>` share links are **unaffected** by any of this: they are
|
||
anonymous by design and keep serving until revoked, so cutting someone's
|
||
access does not kill links they already minted.
|
||
|
||
```sh
|
||
# On the server device (knows the storage)
|
||
bdrive web -c config.json
|
||
|
||
# On each client device (knows only the server) — one command does it all:
|
||
bdrive login https://drive.example.com:4173 # once per device
|
||
cd ~/some-project && bdrive init # once per project
|
||
```
|
||
|
||
`bdrive login` signs the device in and remembers the server (`settings.json`
|
||
under the bdrive home; bare `bdrive login` defaults to beardrive.ai — the
|
||
managed cloud, where signup auto-creates a free personal workspace; pass
|
||
your hub's URL to use a self-hosted hub instead — `--status` shows the
|
||
current server and account). To move to a **different
|
||
hub**, run `bdrive login <new-url>` and then re-run `bdrive init` in each
|
||
folder to connect it to a project there; `bdrive logout` signs out entirely.
|
||
`bdrive init` then, per
|
||
project, walks you through it on a terminal: **create a new project or
|
||
connect an existing one** (picked from the server's list), and **sync the
|
||
whole folder or only some of its subfolders** (e.g. `./wiki`). Every question
|
||
has a flag (`--name`, `--project`, `--only`, `--yes`), and without a TTY
|
||
init never prompts — it creates-or-joins a project named after the folder
|
||
and syncs everything. It writes `.bdrive/config.json`, seeds a starter
|
||
`.bdriveignore` (node_modules, build dirs, caches, `.env*`), and starts the
|
||
daemon — local changes are detected within seconds, and the Claude Code
|
||
plugin syncs at every session step. Not signed in yet? init runs the login
|
||
flow first.
|
||
|
||
Under the hood the `https://` remote speaks the hub's per-project
|
||
`/api/p/<id>/store` API — journal reads/writes relay through the server,
|
||
blob uploads go direct to the object store via the same short-lived
|
||
presigned URLs browser uploads use (falling back to relaying when the
|
||
backend can't presign). Client pushes and project creation require the
|
||
server to run with `--upload`; against a read-only hub, clients still pull
|
||
and `bdrive status` reports `access: read-only (pull only)` rather than
|
||
pretending to be offline.
|
||
|
||
### Sharing files by URL
|
||
|
||
For teammates, every synced file already has an internal link — the hub
|
||
viewer URL, gated by sign-in and the project's org membership:
|
||
|
||
```console
|
||
$ bdrive url wiki/report.html
|
||
https://drive.example.com/p-1a2b3c4d/wiki/report.html
|
||
```
|
||
|
||
It's computed locally (no network), always shows the latest synced
|
||
content, and is the link agents should drop in their replies when they
|
||
create an artifact in the shared folder (`--sync` pushes first so a
|
||
just-created file resolves immediately).
|
||
|
||
For people **outside** the hub, any synced file can instead be shared
|
||
with a public link — hand someone the URL and they see the file, no
|
||
account needed:
|
||
|
||
```console
|
||
$ bdrive share wiki/report.html
|
||
https://drive.example.com/s/eacc1df3ee6a6ebbdacc535c2796dc30
|
||
```
|
||
|
||
Links always serve the file's **latest** synced content (right for wiki
|
||
pages and living reports), and live until revoked — `bdrive share --list`
|
||
and `--revoke <token-or-url>` manage them, `--expires 24h` makes one
|
||
self-destruct. The web UI has a Share button on every file.
|
||
|
||
Shared HTML renders as a real page, markdown renders like the viewer
|
||
(with a small "Shared with BearDrive" footer; raw HTML is served
|
||
byte-for-byte), PDFs open inline. Rendering is sandboxed: `/s/*` responses
|
||
carry a strict CSP, never see auth cookies, and sit behind a generous
|
||
per-IP rate limit (`share_rpm`), so a malicious shared file's scripts
|
||
can't touch hub sessions and a scraper can't turn the hub into a CDN.
|
||
Any org member can mint links, and a link is public to whoever has the
|
||
URL — don't share folders that hold secrets, and note a LAN-bound hub
|
||
means LAN-only links.
|
||
|
||
### Claude Code integration
|
||
|
||
The BearDrive plugin (`/plugin marketplace add runbear-io/beardrive`)
|
||
makes agents fluent in all of this, and **`/beardrive:install`** sets a
|
||
project up conversationally: installs the CLI, signs in, creates or
|
||
connects a project (whole folder or a shared subfolder like `wiki/`),
|
||
offers to document the shared folder in CLAUDE.md so agents proactively
|
||
put shareable artifacts there, and registers hooks in your user config
|
||
(`~/.claude/settings.json`, once per machine) — a blocking pull when you submit a prompt (Claude
|
||
reads fresh team files) and an async push after every file edit (artifacts
|
||
are on the server seconds after Claude writes them), for every teammate
|
||
whether or not they installed the plugin. The payoff: "write a report and
|
||
share it" becomes Claude generating `wiki/report.html` and replying with a
|
||
public URL.
|
||
|
||
The web UI lists your orgs' projects in the sidebar (⌘K opens a command
|
||
palette: fuzzy file search, project switching, share/history/upload
|
||
actions); selecting one browses that project's files, and the **History**
|
||
view shows every change — which
|
||
account made it, when, from which device (name, OS, and the IP the server
|
||
observed), with view/download of any past version (content is
|
||
content-addressed and retained forever; reverting to a version is the next
|
||
phase and the API is already shaped for it). Folder rows have a history
|
||
shortcut for a subtree feed; the topbar button shows the current file's
|
||
versions or the whole project feed.
|
||
|
||
Hubs also track **read heat**: viewer opens and downloads count as human
|
||
reads, share-link hits as share reads, and agent tool reads (reported by
|
||
the sync hooks via `bdrive read-log`) as agent reads — sync replication
|
||
never counts. Folder listings show heat dots and 30-day read counts to
|
||
every member, and every member gets the project **Dashboard**
|
||
(sidebar or ⋯ menu), four sections with an all/human/agent lens: a **treemap** of
|
||
every file (cell size = reads, color = staleness, ⚠ on hot+stale — click
|
||
through to any file), the **reads × freshness** scatter whose hot-but-stale
|
||
quadrant is the knowledge people rely on that nobody maintains, the
|
||
**hot path** (top files by reads, agent/human split — effectively the
|
||
team's agent context window), and an **agent coverage matrix** (which
|
||
agent devices read which folders). The API
|
||
(`GET /api/p/<id>/heat?prefix=&days=`) exposes only aggregate counts,
|
||
distinct-reader counts, and last-read times — never who read what;
|
||
`?by=device` adds the agent-only per-device folder breakdown (device
|
||
identity is already public via history; human emails never appear).
|
||
|
||
### Authentication & database
|
||
|
||
Hubs always require sign-in — every change is attributed to a real
|
||
account. **Signup is invite-only by default** (the safe posture for a
|
||
public URL); self-service signup opens only with a gate (admin approval,
|
||
or allowed domains + email verification). Hub metadata (accounts,
|
||
projects, orgs, shares) lives in a file-backed store by default, or
|
||
SQLite/Postgres (incl. Supabase) via the `database` config block.
|
||
|
||
Full reference — the three signup postures, SMTP, admins, CLI device
|
||
sign-in, and database selection: **[docs/self-hosting.md](docs/self-hosting.md)**.
|
||
|
||
|
||
### Uploads
|
||
|
||
The browser client is deliberately storage-blind: it never sees the remote
|
||
URL, bucket, or any credentials. On page load it fetches `/api/config` and
|
||
follows whatever the server allows.
|
||
|
||
With `--upload` set, the server decides per upload how the bytes travel:
|
||
|
||
- **Direct** — for backends that can presign (S3 and S3-compatible stores;
|
||
GCS when the server runs with credentials that can sign, e.g. a service
|
||
account): the server mints a short-lived presigned `PUT` URL for the
|
||
content-addressed blob (`blobs/<sha256>`), the browser uploads straight
|
||
to the object store, then asks the server to commit. The commit verifies
|
||
the blob actually exists and appends a `put` op to the *server's own*
|
||
journal — the blobs-before-journal ordering and the one-writer-per-journal
|
||
invariant both hold. Expired URLs are refused by the store; the client
|
||
just re-runs init. Direct uploads to a bucket also need a CORS rule on
|
||
the bucket allowing `PUT` from the viewer's origin.
|
||
- **Through the server** — `file://` remotes and plain-folder serving can't
|
||
presign, so the client sends content to the server, which stores it
|
||
(object store + journal, or straight to disk for a served folder, where
|
||
the daemon will pick it up like any local edit).
|
||
|
||
## Claude Code plugin
|
||
|
||
Install beardrive support in Claude Code with two commands:
|
||
|
||
```
|
||
/plugin marketplace add runbear-io/beardrive
|
||
/plugin install beardrive@beardrive
|
||
```
|
||
|
||
The plugin sets up everything at once:
|
||
|
||
- **`/beardrive:install`** — the full team setup, conversationally: CLI,
|
||
sign-in, project init (whole folder or a shared subfolder like `wiki/`),
|
||
a consent-gated agent orientation — a synced `AGENTS.md` mapping the
|
||
shared folder plus a repo-root pointer to it — and sync hooks registered in
|
||
your user config, once per machine.
|
||
- **`/beardrive:init [folder] [--name/--project/--only]`** — just start
|
||
syncing a project; `/beardrive:status` diagnoses problems.
|
||
- **Turn-boundary sync hooks**, registered automatically in your user config
|
||
(once per machine, so every session in every folder is covered): a blocking
|
||
pull when you send a message (Claude always reads fresh files) and an async
|
||
push when the turn ends. The hook no-ops instantly outside BearDrive
|
||
projects, which is what makes a machine-wide registration safe.
|
||
- **No permission gauntlet** — the plugin auto-approves beardrive's own setup
|
||
commands (`init`, `login`, `hooks`, `status`, `sync`, `url`) through a
|
||
`PreToolUse` hook, and only as bare invocations: anything with a shell
|
||
operator falls through to the normal prompt.
|
||
- **The `beardrive` skill** ([plugin/skills/beardrive](plugin/skills/beardrive/SKILL.md)),
|
||
covering init/stop/sync, sharing by URL, backends and credentials,
|
||
selective sync, and troubleshooting. Working in a clone of this repo
|
||
picks the same skill up automatically via `.claude/skills/`.
|
||
|
||
## Other agents: Codex, Gemini CLI, Hermes
|
||
|
||
No terminal needed here either — the setup is one paste. Start the agent in
|
||
the folder you want the files and give it:
|
||
|
||
```
|
||
Follow https://raw.githubusercontent.com/runbear-io/beardrive/main/INSTALL_FOR_AGENTS.md
|
||
to set up BearDrive project <project-id> on <hub-url>. Ask me which folder to sync.
|
||
```
|
||
|
||
The agent fetches [INSTALL_FOR_AGENTS.md](INSTALL_FOR_AGENTS.md) and follows
|
||
it: install the CLI, then one `bdrive init` — which signs in (device code
|
||
when there is no browser), installs the skill, registers the sync hooks and
|
||
prints the project link. The instructions live at that URL rather than
|
||
inside the prompt so they never go stale in someone's copy — and the agent
|
||
handles every deviation (already installed, no Homebrew, sign-in, wrong
|
||
folder). The skill step is the durable part: `SKILL.md` is a cross-agent
|
||
format, and `bdrive skill install` writes the very skill the Claude plugin
|
||
ships to each detected platform's user-level skills directory
|
||
(`~/.codex/skills/beardrive/SKILL.md`, `~/.gemini/…`, `~/.hermes/…`,
|
||
`~/.claude/…`), so from then on "share this file" or "what changed?" just
|
||
works. The hooks step is the one people skip when they copy commands by
|
||
hand, which is exactly why `bdrive init` now does it itself.
|
||
|
||
A project's home page in the web UI shows this with the hub URL and project
|
||
id already filled in (plus the plain-terminal version). `bdrive skill` and
|
||
`bdrive hooks` print what's set up on this machine; re-run either after a CLI
|
||
upgrade to refresh.
|
||
|
||
## How it works
|
||
|
||
```
|
||
working folder ←materialize/scan→ local volume store ←push/pull→ object store
|
||
(real files) ~/.bdrive/volumes/<vol> s3:// gs:// file://
|
||
├─ blobs/ content-addressed (sha256)
|
||
├─ journal/ one append-only op log per device
|
||
├─ state.json what's materialized
|
||
└─ sync.json lamport clock + push cursor
|
||
```
|
||
|
||
- Every change becomes an **op** (`put`/`delete`) in this device's
|
||
append-only journal, stamped with a lamport clock, wall-clock time, device
|
||
ID, and author. File content goes into a content-addressed blob store.
|
||
- A **sync** uploads new blobs, then the journal; it downloads other
|
||
devices' journals and any blobs it's missing. Since each device writes
|
||
only its own journal, there are no concurrent writers per object and any
|
||
dumb object store suffices.
|
||
- The folder's state is a deterministic **replay** of all journals ordered
|
||
by `(lamport, time, device)` — every device converges to the same view.
|
||
Concurrent edits keep the last writer at the path; the loser is preserved
|
||
as a conflict-copy file by the device that detects the overlap.
|
||
- A per-mount **daemon** scans the folder every few seconds (cheap
|
||
size+mtime check) and exchanges with the remote every ~10s — or
|
||
immediately after local edits. Tunable with --scan-interval and
|
||
--remote-interval on the daemon (defaults 3s / 10s).
|
||
|
||
### What beardrive does not sync
|
||
|
||
`.git` directories (per-file LWW would corrupt repositories), `.DS_Store`,
|
||
the `.bdrive` settings file, its own temp files, nested mounts (a
|
||
subdirectory with its own `.bdrive/config.json` syncs only through its own
|
||
project — the parent never scans into it, writes over it, or propagates
|
||
deletes for it), and anything excluded by `.bdriveignore` or omitted from an
|
||
`include` list. Empty directories are not tracked (like git).
|
||
|
||
## Roadmap
|
||
|
||
See [ROADMAP.md](ROADMAP.md) — the public, dated roadmap, including the
|
||
items we'd love help with. Highlights: `beardrive restore <path>@<time>`
|
||
(time travel — all content is already retained), FUSE/NFS mount mode,
|
||
journal compaction & blob GC, per-path access scopes for multi-agent
|
||
setups.
|
||
|
||
## Development
|
||
|
||
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the
|
||
build/test workflow and the rules that matter, [ROADMAP.md](ROADMAP.md)
|
||
for where help is wanted, and [CHANGELOG.md](CHANGELOG.md) for what
|
||
shipped when. Self-hosting a hub: [docs/self-hosting.md](docs/self-hosting.md).
|
||
|
||
```sh
|
||
go build ./...
|
||
go test ./...
|
||
```
|
||
|
||
The integration tests in `internal/syncer` simulate multiple devices syncing
|
||
through a `file://` remote, including offline operation and concurrent-edit
|
||
conflicts. Set `BDRIVE_HOME` to relocate all beardrive state (used heavily in tests).
|
||
|
||
### Web frontend
|
||
|
||
The hub's web UI is a React + TypeScript app in `internal/webapp/frontend`
|
||
(Vite + Tailwind v4 + shadcn/ui components owned in-repo; TanStack
|
||
query/table/virtual, react-hook-form + zod, cmdk, sonner, lucide-react —
|
||
routing stays a small in-repo history router). Its
|
||
**built output is committed** at `internal/webapp/static`, the `go:embed`
|
||
target, so building or `go install`-ing the binary never needs Node.
|
||
|
||
Only when changing `frontend/src`:
|
||
|
||
```sh
|
||
cd internal/webapp/frontend
|
||
npm install
|
||
npm run dev # hot-reload dev server, proxying /api to a local hub
|
||
# (BDRIVE_DEV_PROXY=http://localhost:8993 to point elsewhere)
|
||
npm run build # rebuild internal/webapp/static — commit the result
|
||
npm run e2e # Playwright suite; starts its own seeded hub on :8993
|
||
./check-dist.sh # verify the committed static/ is fresh (pre-release check)
|
||
```
|
||
|
||
## License
|
||
|
||
GNU AGPL-3.0 — Copyright 2026 Runbear, Inc. See [LICENSE](LICENSE).
|
||
|
||
We chose AGPL-3.0 deliberately: it keeps BearDrive fully open and
|
||
self-hostable forever while preventing a cloud provider from offering a
|
||
closed BearDrive-as-a-service. The managed service at beardrive.ai funds
|
||
the project; the code stays open.
|
||
|
||
Everything in this repo is open source and self-hostable: a complete BearDrive
|
||
server for one organization's deployment, teams included. The managed service
|
||
at beardrive.ai is the same core plus what only makes sense as an operated
|
||
service — hosting, PropelAuth SSO, billing and plan quotas, backups, and
|
||
support. Provider-specific and billing code stays out of this repo permanently;
|
||
the server exposes interfaces (`AuthProvider`, `QuotaProvider`) that the
|
||
managed deployment fills in.
|