Every hub project now belongs to exactly one org (file-backed orgs.json,
same load-at-open + atomic-rewrite discipline as the other registries).
Membership (email -> owner|member) gates every per-project route: the
viewer APIs, uploads, history, blobs, shares management, and the device
sync store proxy; /api/projects lists only your orgs' projects, and
project names are now scoped per org. Public share links (/s/) stay
public by design.
Design choices, per the simplest-consistent rule:
- Migration: a pre-org hub sweeps all org-less projects into a "default"
org at startup; ALL existing accounts join it (they could all see every
project before, so anything narrower would lock someone out), oldest
account as owner. Zero manual steps.
- An account in no org that creates a project gets a fresh org named
after itself, so nobody is ever blocked from starting to sync.
- Invites are expiring multi-use links (default 7 days): an owner mints
/#join/<token>, any signed-in account that opens it joins as member.
The web UI shows the org in a sidebar footer (members on click,
Invite button for owners).
bdrive init needed no changes: its connect-existing flow lists projects
through the now-filtered API.
Tests: OrgDB + migration units; a 403/access matrix over every
per-project route; invite mint/redeem flow; and a multi-device syncer
test proving a device holding an org-B token can neither pull org A's
files nor push into its store (sync degrades to Offline, never partial).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Cmd/Ctrl+K opens a palette over the viewer: fuzzy-match any synced file
(word-start and streak-aware scoring, matched letters highlighted),
switch projects, and run context actions (share/history/download of the
open file, project history, upload, sign out). Esc, backdrop click, or
Cmd+K again closes it; arrows + Enter drive it. The overlay CSS now
re-asserts [hidden] since its display:flex otherwise overrides the UA
rule and the palette could never dismiss.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
LICENSE is the verbatim GNU AGPL-3.0 text; the Runbear, Inc. copyright
notice lives in the README license section (kept verbatim so GitHub's
license detection works). README also now documents the OSS/managed
boundary: everything here is self-hostable including teams; the managed
service adds only hosting, SSO, billing/quotas, backups, support.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
bdrive mnt was replaced by bdrive init, but the plugin still taught the
old model — /beardrive:mount even instructed Claude to run a command
that no longer exists.
- /beardrive:mount -> /beardrive:init: login-aware, uses bdrive init
(resume/create/connect, --shared), points at /beardrive:install for
the full team setup.
- Skill description reframed around "start syncing a project"; "mount a
folder" kept as a trigger phrase since people still say it.
- status command, marketplace manifest, README plugin section, and
CLAUDE.md updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Share links (bdrive share <file>, or the web UI's Share button):
- Mint an unguessable public URL (/s/<token>) anyone can open — no
account. HTML renders as a page, markdown gets a standalone shell,
PDFs open inline; ?download=1 attaches.
- Sandboxed: /s/* responses carry CSP `sandbox allow-scripts` + nosniff
and never see auth cookies, so shared content's scripts run in an
opaque origin and can't touch hub sessions.
- Links serve the file's LATEST synced content and live until revoked;
--expires makes self-destructing ones; --list/--revoke manage them.
Re-sharing a file returns the same link. File-backed shares.json.
- CLI resolves the project by walking up to .bdrive/ from the shared
file, warns when the hub address is private (LAN-only links), and
hints when the file hasn't synced yet.
/beardrive:install (plugin command) — team onboarding driven by Claude:
- Ensures the bdrive binary, signs in (bdrive login), runs bdrive init
(whole folder or a shared subfolder like wiki/).
- Asks before appending a CLAUDE.md section that teaches agents to put
shareable artifacts in the shared folder and mint URLs with bdrive
share; asks before registering project-level hooks in
.claude/settings.json: blocking pull at UserPromptSubmit, async push
on PostToolUse Write|Edit — teammates sync with or without the plugin.
- Fix: the plugin hook script still checked for the old `.bdrive` file
and was a silent no-op since the directory change; now checks -d.
Tests: share creation gating, public access + sandbox headers, dedupe,
latest-content semantics, revoke, expiry, markdown/download variants,
list filtering, registry persistence. Docs updated (README sharing +
Claude Code sections, SKILL.md, CLAUDE.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
Authentication (previous phase, now landed together with its follow-ups):
- Email+password+name accounts behind an AuthProvider interface; the OSS
server ships BuiltinAuth only (file-backed auth.json: bcrypt password
hashes + SHA-256 token digests, plaintext never stored; server-owned
/auth/* pages; managed deployments can swap in another provider).
- bdrive login: loopback-callback browser flow (sign-up on the page, the
terminal finishes itself) with a device-code fallback for headless
machines; long-lived revocable device tokens in settings.json.
- Password reset via plain SMTP (stdlib) with a log-link fallback when no
SMTP is configured.
Move-proof projects:
- .bdrive is now a directory; config.json carries a stable mount id.
The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed
by that id — never the folder path — so renames/moves are free.
- The daemon re-reads the project config each tick and exits cleanly
(propagating nothing) when its folder vanishes; the registry self-heals
and the next bdrive command at the new location resumes with zero
spurious changes.
bdrive init is the front door (mnt/umnt removed; bdrive stop pauses):
- Interactive on a TTY (create new / connect existing project from the
server's list; whole folder / shared subfolder via the include list),
full flag bypass (--name/--project/--shared/--yes), never prompts
without a TTY. Runs the login flow first when there is no session.
Default server: beardrive.ai (config.DefaultServer).
Web history (revert-ready):
- Hubs now always require auth; journal ops carry the signed-in account
(user/user_name) alongside the git/OS fallback author.
- File-backed device registry: per-device name, OS, account, and the
public IP the server observed, joined into history at read time.
- GET /api/p/<id>/history?path=|prefix= (newest first) and
GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained
forever, so the next phase's revert is re-putting an old blob.
- UI: History button (file versions or project feed), per-folder history
shortcut, view/download of any past version.
Tests: auth flows (callback, device-code, reset single-use, persistence,
gating), history API + device registry, folder-move survival, registry
self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
The web server (bdrive web) becomes a full sync hub, and client devices
get one-command onboarding — without ever seeing storage info or holding
cloud credentials:
- bdrive web -c config.json: server configurable from a JSON file
(remote/addr/upload/upload_ttl/projects_db); explicit flags win.
- Hub mode: pointing bdrive web at a storage root hosts many projects,
each under <root>/<project-id>/ (remote.Prefixed). Projects live in a
file-backed registry (projects.json — loaded at open, rewritten
atomically per change) with create-or-join-by-name semantics.
- Per-project APIs: /api/projects (list/create/get) and
/api/p/<id>/{tree,file,render,download,upload/*,store/*}. The web UI
grows a project list with per-project browsing and hash deep links.
- Browser uploads and a store proxy for syncing devices: presigned
direct-to-storage PUTs when the backend can sign (S3 presign, GCS V4
signed URLs; expiring, credential-free), relayed through the server
otherwise. Journals are never presigned — only immutable blobs.
Blobs-before-journal and one-writer-per-journal invariants hold.
- https:// remote backend: a device syncs one hub project through
/api/p/<id>/store/* — mnt/sync/daemon/log all work unchanged.
- bdrive login <url>: verify a hub and remember it as the device default
(settings.json). bdrive init: create-or-join a project named after the
folder (--name/--project override), write .bdrive, seed a starter
.bdriveignore, mount, and start the daemon — one command per project.
- Hard-break rename: .beardrive->.bdrive, .beardriveignore->.bdriveignore,
~/.beardrive->~/.bdrive, BEARDRIVE_HOME->BDRIVE_HOME, temp/conflict
prefixes; old names are no longer read.
- Tests: presigning, project registry persistence, store API validation
and gating, project isolation over live HTTP, browser upload flows, and
two-device convergence through a hub (incl. read-only pull-only mode).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
One binary instead of two: cmd/bdrive-web becomes the `web` subcommand
(same flags and positional folder-or-URL argument). Measured cost of
carrying the webapp in the CLI: +1.4 MB on a ~56 MB binary (~2.4%) —
the cloud SDKs dominate either way. Drops the second goreleaser build
and the separate go install path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
Product and project are BearDrive; the CLI binary is bdrive (bdrive mnt,
bdrive sync, ...), the web viewer is bdrive-web. All conventions follow
the full name: .beardrive settings file, .beardriveignore, ~/.beardrive
home, BEARDRIVE_HOME, .beardrive-conflict-* / .beardrive-tmp-* files.
Plugin/skill/marketplace renamed to beardrive (/beardrive:mount).
Module path is now github.com/runbear-io/beardrive — merge only after
renaming the GitHub repo, or go install breaks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
BearDrive is the product and repo brand (open source + future managed
cloud); sfs stays the CLI command, and .sfs/.sfsignore/~/.sfs conventions
are unchanged, so nothing breaks. Brew formula becomes
runbear-io/tap/beardrive (still installs the sfs binary).
Not included here (need the GitHub repo rename first): the Go module
path and repo URLs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
- sfs-web (cmd/sfs-web, internal/webapp): read-only Obsidian-style web UI
serving a local folder (default) or an sfs remote; markdown rendering
with [[wikilinks]], task lists and tables, file downloads with ETags,
per-file provenance from the journals; added to goreleaser builds
- .sfs project file (internal/config): per-folder volume/remote/include
settings that travel with the folder, win over the global registry, and
never sync; daemon picks up edits live
- .sfsignore + include lists (internal/syncer): gitignore-style selective
sync with ! re-includes, applied symmetrically in scan and materialize;
newly ignored files stop syncing without being deleted anywhere
- Claude Code plugin (plugin/, .claude-plugin/): sfs skill, /sfs:mount and
/sfs:status commands, turn-boundary sync hooks (blocking pull on prompt,
async push on stop); installable via the repo's marketplace manifest
- CLAUDE.md and .claude project settings for Claude Code development
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz
Replaces sfs-mount / sfs-remote / sfs-status with a single sfs skill so
Claude Code loads one cohesive guide for the CLI instead of routing
between three. README gains a "Claude Code skill" section that links to
.claude/skills/sfs/SKILL.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One volume can be mounted at several folders (e.g. ./shared in multiple
repos defaulting to the same volume name). Previously the daemon pidfile
and materialization cache were keyed per volume, so mounts fought over a
single daemon and shared one folder-state cache. Key both by a mount ID
(hash of the absolute folder path); blobs, journals, and the lamport
clock stay shared per volume. Content now propagates between co-mounted
folders even offline. Also default remote polling 30s -> 10s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>