๐ฆ Continuous file sync for developers โ like Dropbox, but it respects your .devignore, refuses to leak secrets, runs your hooks, and keeps Git-like history. ๐ฆ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โโโ โโโ โฌ โฌ โโ โโโ โฌ โฌ โ
โ โโโโค โโค โ โ โโดโโ โ โโดโค sync your dev tree, โ
โ โดโโโโโโโ โโโโ โโโโโโ โด โด skip the junk, keep history โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Note
๐ devbox v1 is feature-complete (M0โM7.6, fleet-verified on real hardware โ Macs + arm64 Pis live-syncing through a hub on a NAS) and v2 has begun. ๐ฎ The v1 spec is fully implemented; the v2 spec sequences M8โM11 โ M8 foundations are landing now. Star/watch and follow along. โญ
| Milestone | Status |
|---|---|
| M0 โ Skeleton (CLI, identity, config) | โ done |
M1 โ Watch ยท .devignore ยท secret-guard ยท chunking ยท manifest |
โ done |
| M2 โ Hub + one-way push (deployed + verified cross-machine ๐ฐ๏ธ) | โ done |
| M3 โ Two-way sync ยท SSE fan-out ยท 3-way conflict copies ยท live daemon | โ done โ two real Macs sync live through the hub ๐ |
| M4 โ Read-only mounts ยท sub-path mounts ยท bandwidth cap | โ done โ fleet-verified |
| M5 โ Lifecycle hooks (pre/post push/pull, on-conflict) | โ
done โ post-pull ran on a real fleet node ๐ช |
M6 โ Versioning: log / restore + hub GC |
โ done โ restore reverted a file on the fleet ๐ฐ๏ธ |
M6.5 โ devbox deploy (pin a mount to a snapshot, no push) |
โ done โ blue/green-deployed v1 on a real box while head stayed v2 ๐ |
M7 โ Hardening: devbox doctor, reconnect/backoff, rescan fallback, name-clash, release builds |
โ done โ doctor/stop/hooks + share-name guard + dead-watcher rescan fleet-verified ๐ก๏ธ |
| M7.5 โ Adversarial security/data-loss audit + fixes (path-traversal, blob integrity, never-clobber, safe GC) | โ done โ 26 findings, all promise-breakers fixed, race-clean ๐ |
| M7.6 โ Hardening: fsync durability ยท DoS caps + timeouts ยท pidfile PID-reuse guard ยท join proof-of-possession | โ done โ fleet-verified on arm64 ๐ก๏ธ |
๐ฎ M8 โ v2 Foundations: migration runner ยท per-(share,id) snapshots ยท control socket + pause/resume ยท M8a teams ยท restore byte-safety |
โ
justified-now scope complete & fleet-verified (Pi .13 owner invited an editor, Pi .15 redeemed & pushed); 3 migrations verified on a copy of the real hub DB. M9โM11 stay demand-driven ๐๏ธ๐ฅ |
| ๐ค Why devbox? | ๐ง Core Concepts | ๐๏ธ Architecture |
| ๐ How Sync Works | ๐ฅ Conflicts | โก Install & Run |
| ๐งฐ CLI Reference | ๐ Live Dashboard | ๐ช Hooks |
| ๐ฐ๏ธ Versioning & Deploy | ๐ฅ๏ธ Cross-Platform | ๐ Security & Durability |
| ๐บ๏ธ Roadmap | โ๏ธ License & Open-Core | ๐ Contributing |
You've got a MacBook, a 40-node Pi cluster, and a TrueNAS box. You want your active working tree mirrored across them โ right now, automatically โ without committing half-done work.
Your options today all hurt:
| Tool | The pain ๐ |
|---|---|
| โ๏ธ Dropbox / iCloud | Syncs everything blindly โ chokes on node_modules, happily uploads your .env, thrashes on build artifacts. |
| ๐ Git | Manual. Commit-based. Not built to live-mirror an in-progress working tree. Branching โ syncing. |
| ๐ rsync / scp | One-shot, one-direction, no history, no hooks, no conflict safety. You babysit it. |
| ๐ Syncthing | Great P2P sync โ but no .devignore dev-ergonomics, no lifecycle hooks, no snapshot/deploy story. |
| ๐ฏ | Feature |
|---|---|
| ๐ | Continuous bidirectional sync across all your machines |
| ๐ | .devignore (gitignore syntax) โ skip node_modules, dist, the junk |
| ๐ | Default-on secret guard โ hard-refuses to upload .env, keys, secrets |
| ๐ช | Lifecycle hooks โ pnpm install / restart a container when files land |
| ๐ฐ๏ธ | Git-like snapshots per share โ roll back any file, any time |
| ๐๏ธ | Selective mounts โ cherry-pick a share, or just one sub-path, per machine |
| ๐ฅ | Conflict-safe โ never blocks, never asks, never loses a byte |
| ๐ | Self-hostable โ one Go binary on your Pi/TrueNAS, no SaaS required |
| ๐ | Cross-platform โ Linux ยท macOS ยท Windows |
mindmap
root((๐ฆ devbox))
๐ฐ๏ธ Hub
publishes shares
stores chunks + manifests CAS
brokers change events
one owner ยท many devices
๐ป Device
Ed25519 identity
joins with a token
runs one daemon
๐ Share
named tree projects/ repos/ services/
unit of snapshot history
๐ Mount
share + subpath to local path
read-write or read-only
๐ธ Snapshot
immutable BLAKE3 manifest
per share history
| Term | What it is |
|---|---|
| ๐ฐ๏ธ Hub | The server. Publishes shares, stores content-addressed chunks + manifests, brokers events. One owner, many devices. Self-hosted single binary. |
| ๐ป Device | A machine with an Ed25519 identity, joined to a hub. Runs one daemon (devboxd). |
| ๐ Share | A named top-level tree on the hub (projects, repos, services). The unit of permission & history. |
| ๐ Mount | A device-side binding: share[/subpath] โ localpath, read-write or read-only. One daemon watches many. |
| ๐ธ Snapshot | An immutable, per-share manifest version (id = BLAKE3 of the manifest). |
flowchart LR
subgraph LAP["๐ป MacBook"]
D1["devboxd ๐ก๏ธ"]
M1["๐ projects/"]
M2["๐ repos/"]
end
subgraph HUB["๐ฐ๏ธ Hub ยท Pi / TrueNAS"]
WS{{"๐ WebSocket\nchange events"}}
HTTP{{"๐ HTTP\nblob GET/PUT"}}
CAS[("๐ง CAS\nchunks + manifests\nBLAKE3")]
DB[("๐๏ธ SQLite WAL\nshares ยท snapshots\ndevices ยท tokens")]
MET["๐ /metrics + status"]
end
subgraph PI["๐ pi-07"]
D2["devboxd ๐ก๏ธ"]
M3["๐ projects/p22/backend\n(read-only โ /var/www)"]
end
M1 & M2 -->|"events"| WS
M1 & M2 -->|"blobs"| HTTP
WS --> CAS
HTTP --> CAS
CAS --- DB
WS -->|"events"| D2
HTTP -->|"blobs"| D2
D2 --> M3
classDef hub fill:#0d1117,stroke:#4F9CF9,stroke-width:2px,color:#fff;
class HUB,WS,HTTP,CAS,DB,MET hub;
๐ก Why two channels? WebSocket for live events (tiny, ordered) + stateless HTTP for blobs (
GET /blob/<hash>โ range/resume/caching for free). Same TLS endpoint, same token. Pure-WebSocket would force us to reinvent resumable transfer over a socket โ more code.
sequenceDiagram
autonumber
participant FS as ๐ Filesystem
participant D as ๐ก๏ธ devboxd
participant H as ๐ฐ๏ธ Hub
participant P as ๐ Peer
FS->>D: โ๏ธ file changed (fsnotify, debounced ~300ms)
D->>D: ๐ .devignore + ๐ secret-guard filter
D->>D: ๐งฉ FastCDC chunk โ BLAKE3 โ manifest diff
D->>D: ๐ช pre-push hook (can veto ๐)
D->>H: โฌ๏ธ upload missing chunks (HTTP, bw-capped)
H->>H: ๐ธ append snapshot, advance share HEAD
H-->>D: โ
ack
D->>D: ๐ช post-push hook
H->>P: ๐ change event (WebSocket)
P->>P: ๐ช pre-pull hook (can veto ๐)
P->>H: โฌ๏ธ fetch missing chunks (HTTP)
P->>P: ๐งฑ reassemble โ โ๏ธ atomic rename into place
P->>P: ๐ช post-pull hook (pnpm install / restart ๐)
A read-only mount skips steps 1โ8 (it never pushes) but still receives and applies inbound changes. ๐
devbox is Dropbox-easy (never nags you mid-work) and data-loss-proof. Here's the magic: the hub keeps a linear HEAD per share, and every push declares the snapshot it was based on.
flowchart TD
START([๐ค push arrives]) --> Q{parent == HEAD?}
Q -->|โ
yes| FF["โฉ fast-forward\nadvance HEAD"] --> DONE([๐ done])
Q -->|โ no| THREE["๐ฑ per-file 3-way\nvs common ancestor"]
THREE --> ONLY{changed by\nonly the pusher?}
ONLY -->|yes| CLEAN["โจ applies cleanly"] --> DONE
ONLY -->|both sides| CONF["๐ฅ CONFLICT"]
CONF --> KEEP["๐ first-to-land stays canonical"]
CONF --> COPY["๐ loser saved as\nfoo.conflict-laptop-1719.go"]
CONF --> HOOK["๐ on-conflict hook fires"]
KEEP & COPY & HOOK --> SAFE([๐ zero data lost])
style CONF fill:#5a1e1e,stroke:#ff6b6b,color:#fff
style SAFE fill:#1e5a2e,stroke:#51cf66,color:#fff
style DONE fill:#1e5a2e,stroke:#51cf66,color:#fff
๐ The classic "laptop was offline" scenario โ click to expand
Both machines synced at snapshot S3, both have foo.go v1:
- ๐ Laptop goes offline, edits
foo.goโ v2-laptop (queued, parent stillS3). - ๐ Pi (online) edits the same
foo.goโ v2-pi, pushes. Hub HEADS3 โ S4. Pi's wins canonical. - ๐ Laptop reconnects, pushes with
parent=S3โ but HEAD isS4.parent โ HEADโ conflict path. - ๐ฑ Both changed
foo.gosinceS3โ real conflict. - ๐ Result, nothing destroyed: both machines end up with
foo.go= v2-pi (canonical) andfoo.conflict-laptop-1719.go= v2-laptop beside it.
The offline edit is never lost โ it just lands as a clearly-named sibling. ๐ฏ
Conflict rules at a glance:
| Situation | Outcome |
|---|---|
| ๐ค Both edit same file | First-to-land canonical; loser โ .conflict-<host>-<ts> copy |
| ๐๏ธ One deletes, one edits | Edit always wins โ a delete has no bytes to lose |
| ๐ค Rename | Free โ old-gone + new-added; content-addressed = zero re-transfer |
| ๐ Read-only mount about to clobber a local edit | Local stashed as .conflict-local-<ts> first |
| ๐ You find out via | devbox status badge ยท on-conflict hook ยท devbox conflicts list |
๐ซ No blocking prompts. A headless daemon can't prompt you, and nagging would break the whole "Dropbox-easy" promise. You get told; you choose when to look.
Client (macOS / Linux) โ one line. Detects your platform, lets you pick where the binary lives, and offers a keep-alive auto-restart service (launchd KeepAlive on mac, systemd Restart=always on Linux):
curl -fsSL https://git.shoemoney.ai/shoemoney/devbox-dist/releases/download/latest/install.sh | sh
# non-interactive: DEVBOX_BIN_DIR=~/.local/bin DEVBOX_SERVICE=1 sh install.sh
# also install the hub: sh install.sh --hub๐ช Windows ยท ๐ macOS Full Disk Access ยท ๐ ๏ธ knobs
- Windows:
irm https://git.shoemoney.ai/shoemoney/devbox-dist/releases/download/latest/install.ps1 | iexโ copiesdevbox.exe, adds it to PATH, and (optionally) a restart-on-failure Scheduled Task at logon. Notes Controlled Folder Access if you have it on. - From Go (have a toolchain):
go install github.com/shoemoney/devbox/cmd/devbox@latest(hub:โฆ/cmd/devbox-hub@latest). - macOS Full Disk Access: to sync
~/Desktop,~/Documents,~/Downloads, or iCloud, grant Full Disk Access to thedevboxbinary (System Settings โ Privacy & Security โ Full Disk Access).devbox doctortests your actual mounts and tells you the exact fix + a deep link โ a background daemon can't show the per-folder prompt, so without it those mounts silently fail. - Knobs:
--bin-dir DIRยท--hubยท--service/--no-serviceยท--release-url URL(or theDEVBOX_*env equivalents). No prebuilt release? The script falls back to a localdist/(runscripts/build-release.sh) orgo build. - Service control:
systemctl --user {status,disable} devbox(Linux) ยทlaunchctl unload ~/Library/LaunchAgents/ai.shoemoney.devbox.plist(mac).
Hub on a NAS (TrueNAS / Synology / unRAID / any Docker host) โ self-healing container, drop-in:
docker compose up -d --build # hub on :8088, data persisted, restart: unless-stopped
docker compose exec hub devbox-hub token --data /data # mint a join token
# live dashboard: uncomment the :8099 port + --dashboard command in docker-compose.yml
# stronger self-heal (restart on unhealthy): docker compose --profile selfheal up -dThe image is a tiny (~32 MB) static, non-root Alpine build โ Dockerfile + docker-compose.yml at the repo root.
๐ Production: TLS + safe upgrades
Put the hub behind TLS. The hub speaks plain HTTP and the bearer token + your file bytes cross the wire in the clear โ front it with a reverse proxy (nginx / Caddy / Nginx Proxy Manager) and terminate TLS there (the API is bearer-authed). Two settings a sync hub needs in the proxy:
location / {
proxy_pass http://<hub-host>:8088;
client_max_body_size 0; # large blob uploads โ never cap them
proxy_buffering off; # stream the /v1/events SSE to clients
proxy_read_timeout 3600s; # long-lived event stream
}Devices then devbox join https://hub.example.com <token> โ Go's client does TLS natively.
Upgrade safely. The hub DB is the canonical index to every share's history, so don't hot-swap the binary. deploy/redeploy-hub.sh runs the never-YOLO pipeline: migration dry-run on a copy of live data (scripts/verify-hub-migration.sh) โ auth smoke the new binary in a throwaway instance (scripts/hub-auth-smoke.sh) โ back up binary + DB โ swap โ verify the new code is live โ auto-rollback on any failure. (--gc-every <dur> self-maintains so blobs don't accumulate.)
โ Every command below is implemented and fleet-tested. Build with
scripts/build-release.sh.
# ๐ฐ๏ธ On your hub (Pi / TrueNAS / NAS)
devbox-hub serve --data /srv/devbox --listen 0.0.0.0:8088
devbox-hub token # prints a one-time join token
# ๐ป On your laptop
devbox join http://hub.lan:8088 <token> # enroll this machine
devbox publish ~/Projects projects # create a share from a folder
devbox start # live-sync daemon (foreground)
# ๐ On another machine โ clone the share and keep it in sync
devbox join http://hub.lan:8088 <token>
devbox mount projects ~/Projects # clone + register the mount
devbox start
# ๐ A read-only deploy box โ pulls, never pushes its runtime cruft back up
devbox mount api /var/www/api --ro
devbox startThat's it. โจ Edit on your laptop โ it lands on the Pi in near-real-time, node_modules stays
home, your .env never leaves the building, and post-pull can pnpm install + restart your
container automatically. ๐ช
๐ป Device commands
| Command | What it does |
|---|---|
devbox setup |
๐งญ Step-by-step first-run wizard (also auto-offered once on a fresh machine; opt out with "don't ask again") |
devbox join <hub> <token> |
๐๏ธ Enroll this device against a hub |
devbox mount <share> <dir> |
๐ Mount a share into a local dir (clone + sync) |
devbox mount <share> <dir> --ro |
๐ Mount read-only (pull only, never push) |
devbox mount โฆ --exclude <pat> |
๐ Device-local ignore (gitignore syntax, repeatable) layered on the shared .devignore โ e.g. skip a local build dir |
devbox publish <dir> <name> |
๐ Create a share from a local folder + push it |
devbox unmount <share> |
|
devbox start / stop |
|
devbox status [--json] |
๐ Device, hub, mounts (with ro/pinned), per-mount sync-age and last sync error (โ ๏ธ last sync failed: โฆ); --json exposes last_err (prefers live daemon state) |
devbox log <share> [--json] |
๐ฐ๏ธ Snapshot history (full ids); --json for machine-readable output |
devbox restore <share> <snap> [path] |
โฉ๏ธ Roll back a file or a whole share |
devbox deploy <share> <snap> |
๐ Pin a mount to a snapshot โ applies it without pushing (blue/green) |
devbox conflicts [--json] [--rm] |
๐ฅ List conflict copies across all mounts; --json emits a JSON array, --rm deletes them (review first!) |
devbox ignore <pattern> |
๐ Append a pattern to ./.devignore (rejects a pattern that doesn't compile) |
devbox hook edit <share> <event> |
๐ช Scaffold/open a hook in $EDITOR; hook list <share> shows installed |
devbox doctor [--json] |
๐ฉบ Diagnose watcher limits, perms, bash, hub connectivity + bearer + clock skew vs hub (warns >30s) + free disk on each mount/config dir (warns <1 GiB); non-zero exit on โ โ cron-friendly; --json for monitoring |
devbox status shows sync age |
โฑ๏ธ Live status now prints per-mount last-sync age ("synced 12s ago" / "not synced yet") |
devbox pause [--for <dur>] / resume |
โธ๏ธ--for 2h auto-resumes (M8) |
devbox invite <share> <principal> <role> |
โ๏ธ Mint an invite token granting a role (--reshare for +s); attenuation-enforced (M8a) |
devbox invite revoke <token> |
๐๏ธ Kill a pending invite before it's redeemed (only a caller who could mint it) |
devbox members <share> |
๐ฅ Show who can access a share, or "legacy share" (M8a) |
devbox-hub member set/rm/list ยท principal |
๐ก๏ธ Hub-side role admin (M8a) |
devbox peers |
๐ Planned โ needs a hub peers endpoint (M10) |
๐ฐ๏ธ Hub commands (run on the hub host)
| Command | What it does |
|---|---|
devbox-hub serve --config <file> |
๐ Start the hub |
devbox-hub token |
๐๏ธ Mint / rotate the join token |
devbox-hub device ls [--json] ยท revoke <id> |
๐โ List enrolled devices (id/name/principal/last-seen/revoked) / revoke one |
devbox-hub share ls [--json] |
๐ List shares (name/head/ACL mode/#snapshots/#members/updated) |
devbox-hub readonly <device> <share> |
๐ Mark a device read-only on a share |
devbox-hub member set/rm/list ยท principal |
๐ก๏ธ Per-share roles + principals (M8a) |
devbox-hub backup <dir> |
๐พ Disaster-recovery snapshot: consistent DB copy (VACUUM INTO) + the blob tree into <dir> โ flocked against gc so a sweep can't strand it mid-copy |
devbox-hub fsck [--json] |
๐ฌ Integrity scan: re-hash every blob (bit-rot) and flag dangling snapshots (manifest โ missing blob); non-zero exit on either โ pairs with backup for DR confidence |
devbox-hub serve --dashboard |
๐ Serve the live web dashboard (loopback :8099 by default) |
devbox-hub serve --dashboard-token <tok> |
๐ Require a token to view the dashboard (recommended for any non-loopback bind) |
devbox-hub serve --metrics-token <tok> |
๐ Require a token for /metrics โ close the unauthenticated leak on a WAN-exposed hub |
devbox-hub serve --access-log |
๐ Log one line per request (method, path, status, bytes, addr, device id) for WAN forensics |
devbox-hub serve (startup) |
๐ฐ๏ธ Logs build version + dashboard state; --listen is non-loopback (plain HTTP โ cleartext bearers without the TLS proxy) |
devbox-hub serve (SIGTERM) |
๐ Graceful shutdown โ drains in-flight requests via http.Server.Shutdown and drops live SSE streams immediately (no 10s wait), so the hardened redeploy swaps cleanly |
devbox-hub serve --gc-every <dur> |
๐งน Opt-in in-process periodic GC (off by default; each sweep animates on the dashboard) |
devbox-hub gc [--dry-run] [--keep <n>] [--keep-days <n>] |
๐งน GC unreferenced chunks; --dry-run previews, --keep keeps N newest/share, --keep-days also keeps anything from the last N days |
GET /healthz ยท GET /readyz |
๐ฉบ Liveness (/healthz reports the build version) / readiness (/readyz pings the DB โ 503 if unreachable) for Docker/LB |
GET /metrics |
๐ Prometheus: gauges (devices/shares/snapshots/chunks) + counters (blob bytes in/out, pushes, conflicts); gate with --metrics-token |
Our own. No Grafana, no Prometheus required, no external deps โ it ships inside the hub binary and runs air-gapped. Off by default; flip it on and watch every push, join, and chunk flow in real time. โจ
devbox-hub serve --dashboard # loopback http://127.0.0.1:8099 (safe default)
devbox-hub serve --dashboard --dashboard-addr 0.0.0.0:8099 # expose on the LAN (unauthenticated โ warns you)
devbox-hub serve --dashboard --gc-every 24h # also self-maintain (in-process GC) โ each sweep animatesA mission-control flow visualization rendered on <canvas> at 60fps: a breathing hub core, your
devices orbiting (bright = active), shares on the outer ring โ and the fabric comes alive with five
flow events: push (device โ hub โ share, fans out to subscribers), pull (a calm teal pulse inbound as a
device syncs), conflict (a red collision burst when a stale push is rejected), gc (an amber hub-wide sweep
- toast when garbage-collection runs), and
join. Plus live metric tiles and a server-side history sparkline (stacked push/pull/conflict/gc per minute, last hour) that survives a page reload, and a streaming, color-coded activity feed.
| How it works | |
|---|---|
| ๐ฅ๏ธ Served from the hub | single go:embed'd HTML, vanilla JS + canvas, zero CDN/build |
๐ GET /api/state |
snapshot: hub, totals, devices, shares + a 60-minute history window (UI polls every 10s) |
๐ก GET /api/events |
SSE live flow stream (join ยท push ยท pull ยท conflict ยท gc) โ the dashboard animates each one |
| ๐ Loopback by default | unauthenticated read-only metrics; bind non-loopback only deliberately (it warns) |
๐ฌ ?demo=1 |
synthesizes a live stream of all five event types โ instant wow + works offline (file://) |
flowchart LR
DEV["๐ป device pushes"] -->|"POST /v1/push"| HUB["๐ฐ๏ธ hub"]
HUB -->|"Emit(join/push)"| BRK["๐ก SSE broker"]
BRK -->|"/api/events"| UI["๐ dashboard canvas"]
HUB -->|"/api/state"| UI
UI -.->|"particle deviceโhubโshare"| UI
style HUB fill:#0b2b3a,stroke:#27e8ff,color:#fff
style UI fill:#2a0b2b,stroke:#ff3df0,color:#fff
style BRK fill:#1e5a2e,stroke:#b6ff3a,color:#fff
Drop executable scripts in <mount>/.devbox/hooks/, named after the event. bash everywhere
(a .ps1 hook auto-runs via pwsh on Windows ๐ช). pre-* non-zero exit aborts the step.
60s timeout โ a hung hook is killed, never wedges the loop. โฑ๏ธ
| Hook | Fires | Abort? | Typical use |
|---|---|---|---|
pre-push |
before upload | โ | ๐งน lint/format, secret scan |
post-push |
after upload | โ | ๐ฃ notify, log, tag a snapshot |
pre-pull |
before apply | โ | ๐ stop a container / dev server |
post-pull |
after apply | โ | ๐ฆ pnpm install, migrate, restart |
on-conflict |
conflict copy made | โ | ๐ open a diff, ping you, log |
#!/usr/bin/env bash
# .devbox/hooks/post-pull โ reinstall deps + restart only when needed
if grep -qE 'package\.json|pnpm-lock\.yaml' "$DEVBOX_CHANGED_FILES"; then
pnpm install --frozen-lockfile
fi
docker compose restart app # ๐๐ฑ Injected environment variables
DEVBOX_EVENT=post-pull
DEVBOX_MOUNT=/srv/project
DEVBOX_SHARE=projects
DEVBOX_HOST=pi-node-07
DEVBOX_CHANGED_FILES=/tmp/devbox-changes.txt # newline-delimited
DEVBOX_SNAPSHOT=ab12cd34
DEVBOX_REMOTE=hub.shoemoney.ainode_modules/ # ๐ฆ the usual suspects
dist/ build/ .next/ target/
*.log *.tmp .DS_Store
!.env.example # โ negate to force-includeMatched paths are invisible to sync in both directions. Change it โ rescan; newly-ignored files are left on disk (never deleted), they just stop syncing.
Important
Your pitch is "won't leak your .env" โ so devbox enforces it. A built-in deny-list runs
in the push path and hard-refuses to upload matched files even if .devignore is
misconfigured. Belt and suspenders. ๐ฉฒ
Default-blocked: .env ยท .env.* (except .env.example) ยท *.pem ยท *.key ยท id_rsa* ยท
*.p12 ยท *.pfx ยท secrets/ ยท *.kdbx ยท common cloud-cred files. Blocked files show up in
devbox status. Add your own via [secrets].extra_patterns in config.toml.
โ๏ธ config.toml โ per-machine tunables
[transfer]
max_kbps = 0 # blob transfer cap (KB/s); 0 = unlimited
compress = false # ๐ gzip blobs BOTH ways (upload + download) โ turn on for devices syncing over a WAN link
[sync]
rescan_seconds = 60 # watcher-fallback rescan cadence; raise on a huge tree, lower for snappier convergence
ignore_defaults = false # ๐ also ignore common junk (node_modules, .git, target, dist, build, .venv, __pycache__, โฆ)
[secrets]
extra_patterns = ["*.secret", "vault-*"] # additional secret-guard deny patterns
compressonly gzips a chunk when it actually shrinks (incompressible blobs go raw), and the hub hashes the decompressed bytes so dedup + integrity are untouched. ๐๏ธ The WAN path is also hardened with granular timeouts, automatic retry+backoff on transient blips, and parallel blob transfer โ no flags, always on.
gitGraph
commit id: "S1 ๐ธ"
commit id: "S2 ๐ธ"
commit id: "S3 โ๏ธ"
commit id: "S4 ๐"
commit id: "S5 ๐ deploy"
- ๐ธ Every accepted change = an immutable snapshot (BLAKE3 of the manifest). Manifests are themselves content-addressed โ 100 pushes don't store the tree map 100ร.
- โฉ๏ธ
devbox restore <snap> [path]rolls back a file or whole share (itself a new change โ reversible). - ๐
devbox deploy <share> <snap>pins a mount to a snapshot by applying it without pushing a new head โ history stays untouched and the daemon won't drag it back to latest ([pinned]). Blue/green deploys for your/var/wwwboxes, basically free; re-mount to resume live sync. - ๐งน
devbox-hub gcsweeps unreferenced chunks (refcounted).
| Capability | ๐ง Linux | ๐ macOS | ๐ช Windows |
|---|---|---|---|
| File watching | inotify | FSEvents | ReadDirectoryChangesW |
| Atomic apply | rename(2) |
rename(2) |
ReplaceFile/MoveFileEx |
| Hooks | bash | bash | bash (git-bash/WSL) or .ps1โpwsh |
| Service | systemd | launchd | Windows Service |
| Static binary | โ | โ | โ |
๐งญ Canonical paths are forward-slash + relative (converted at the Windows boundary). Filenames illegal/colliding on an OS (
foo.govsFoo.go,aux, trailing dot) โ skip + warn + surface indevbox status; the hub keeps the bytes, peers that can hold the name still get the file. Never fatal. ๐
Threat model: single-owner, multi-device (every enrolled device is yours). Within that, v1 went through an adversarial audit โ every data-loss and arbitrary-file path is closed. ๐ก๏ธ
| Layer | Protection |
|---|---|
| ๐ชช Device identity | ed25519 keypair per device; join requires proof-of-possession (a signed challenge โ you can't claim a key you don't hold), and a bad request never burns the one-time token |
| ๐๏ธ Auth | bearer tokens, hashed at rest (hub stores no plaintext creds), device-revocable |
| ๐ง Content integrity | every chunk and manifest is re-verified against its BLAKE3 hash on download โ a corrupt/truncated transfer or hostile hub can't write wrong bytes into your tree |
| ๐ง Path safety | hub rejects any blob key that isn't 64-hex (no ..%2f traversal โ no arbitrary file read); the client refuses manifest paths that escape the mount root |
| ๐ Secret guard | case-insensitive deny-list (.ENV == .env); .env/keys/*.env/.aws/credentials never leave the machine, independent of .devignore |
| ๐ Never lose a byte | losing local edits become .conflict copies; ignored/guarded on-disk files are preserved before any hub overwrite; atomic writes are fsync'd (power-loss safe) |
| ๐งน Safe GC | mark-and-sweep from every live head โ never frees a chunk a share still needs, even if refcounts are off |
| ๐ช DoS bounds | request-body caps (256 MiB blob / 8 MiB JSON โ 413) + server read/idle timeouts |
| ๐ Daemon | single-instance pidfile with a PID-reuse guard (start-time token) so stop never signals a stranger |
๐ฌ how the integrity + path guards chain
flowchart LR
P["๐ฅ pull"] --> G{"64-hex<br/>blob key?"}
G -->|no| X1["๐ซ 404 โ no traversal"]
G -->|yes| F["โฌ๏ธ fetch blob"]
F --> H{"BLAKE3<br/>matches key?"}
H -->|no| X2["๐ซ integrity fail"]
H -->|yes| C{"path inside<br/>mount?"}
C -->|no| X3["๐ซ refuse escape"]
C -->|yes| W["โ๏ธ atomic write + fsync"]
style X1 fill:#5a1e1e,stroke:#ff6b6b,color:#fff
style X2 fill:#5a1e1e,stroke:#ff6b6b,color:#fff
style X3 fill:#5a1e1e,stroke:#ff6b6b,color:#fff
style W fill:#1e5a2e,stroke:#51cf66,color:#fff
๐ฎ Looking ahead: the full v2 design spec โ multi-owner teams + ACLs, client-side E2E encryption (convergent, keeps dedup), LAN peer chunk-exchange + hub HA, 3-way merge, and a TUI โ sequenced M8โM11 by dependency. M8 foundations are landing now; M9โM11 stay demand-driven (build E2E when an untrusted-hub user exists, P2P when the uplink hurts, the TUI when asked).
flowchart LR
M6["M6 ๐ฐ๏ธ\nVersioning"] --> M65["M6.5 ๐ข\nDeploy"]
M65 --> M7["M7 ๐ก๏ธ\nHardening\n+ M7.5/7.6 audit"]
M7 --> M8["M8 ๐๏ธ\nv2 Foundations\nmigration ยท teams ยท control socket"]
M8 --> M9["M9 ๐\nTrust + HA\nACL ยท E2E ยท S3"]
M9 --> M10["M10 ๐ค\nCluster + merge\nP2P ยท resolver"]
M10 --> M11["M11 โจ\nPolish\nTUI ยท power"]
style M6 fill:#1e5a2e,stroke:#51cf66,color:#fff
style M65 fill:#1e5a2e,stroke:#51cf66,color:#fff
style M7 fill:#1e5a2e,stroke:#51cf66,color:#fff
style M8 fill:#1e5a2e,stroke:#51cf66,color:#fff
style M9 fill:#0d1117,stroke:#4F9CF9,color:#fff
style M10 fill:#0d1117,stroke:#4F9CF9,color:#fff
style M11 fill:#0d1117,stroke:#4F9CF9,color:#fff
| Milestone | Deliverable | |
|---|---|---|
| โ | M0 โ Skeleton ๐ฆด | cobra CLI, devbox join, keypair, machine config |
| โ | M1 โ Watch + secrets ๐ | fsnotify, .devignore, secret-guard, FastCDC+BLAKE3 chunking, content-addressed manifests |
| โ | M2 โ Hub + push ๐ฐ๏ธ | shares, join tokens, CAS, publish, HTTP upload, snapshots, bearer auth, /metrics โ deployed on .10, verified cross-machine |
| โ | M3 โ Two-way sync ๐ | SSE event fan-out, mount, pull + atomic apply, per-file 3-way conflict copies, live start daemon โ two Macs sync live through the hub |
| โ | M4 โ Read-only + bw ๐ | server-enforced read-only bit, sub-path mounts (mount proj/app /dir), bandwidth cap โ fleet-verified |
| โ | M5 โ Hooks ๐ช | bash (+.ps1) lifecycle runner, env injection, 60s timeout, pre-* veto โ post-pull fired on a fleet node |
| โ | M6 โ Versioning ๐ฐ๏ธ | devbox log (full snapshot ids) / restore (revert any file) / hub gc โ fleet-verified |
| โ | M6.5 โ Deploy ๐ | devbox deploy <share> <snapshot> โ apply a snapshot without pushing, [pinned] mount; fleet-verified blue/green |
| โ | M7 โ Hardening ๐ก๏ธ | devbox doctor, stop/pidfile, hook edit/list, SSE backoff+jitter, 60s rescan fallback (survives dead/limited inotify watchers โ PRD risk #1), share-name guard, release builds โ fleet-verified |
| โ | M7.5 โ Audit hardening ๐ | adversarial security/data-loss audit + fixes: blob-hash path-traversal blocked, download blob-integrity check, manifest-path containment, secret-guard case-insensitive + more patterns, never-clobber ignored/guarded files, GC made safe vs cross-share refcount undercount โ all with regression tests, race-clean, fleet-verified |
| โ | M7.6 โ Hardening complete ๐ก๏ธ | ๐ฝ fsync durability on atomic writes (power-loss safe), ๐ช request size caps + server timeouts (DoS), ๐ pidfile PID-reuse guard (start-time token), ๐ชช join proof-of-possession (ed25519 signature, token not burned on a bad request) โ fanned out to parallel worktree agents, regression-tested, race-clean, fleet-verified |
| โ | M8 โ v2 Foundations ๐๏ธ | ๐ schema migration runner (PRAGMA user_version, transactional, VACUUM INTO backup, refuses a newer DB) ยท ๐ข per-(share,id) snapshots (fixes the cross-share refcount undercount; GC + head-backfill reworked) ยท ๐๏ธ daemon control socket (Unix socket, HTTP/1.1, 0600) wiring devbox pause/resume + live-socket-aware status ยท ๐ฅ M8a: principals + per-share roles + write enforcement (devbox-hub member/principal; legacy shares = v1, first grant flips to deny-by-default; roleโฅeditor AND the writable clamp). Both migrations verified on a copy of the real .10 hub DB (counts preserved, 3 legacy heads repaired, chains 0โ1โ2). ๐ read side (GET /v1/members + devbox members) ยท โ๏ธ device-facing invites (POST /v1/invite + devbox invite, privilege attenuation via pure meta.MayGrant, self-seed on the legacyโexplicit flip) โ cross-machine fleet-verified on arm64 Pis ยท ๐ restore preserves uncommitted edits (never-lose-a-byte on revert). M8's justified-now scope is complete โ the conflict sidecar moves to M10 (only its resolver consumes it) and read-side ACL gating is M9 |
๐ฎ still ahead in M8 / genuinely v2
Landed in M8 above: the migration runner, per-(share,id) snapshots, and the control socket. Still sequenced ahead:
- ๐ Conflict-copy on explicit
restore/deploy(M8-3) โ needs base/ancestor awareness so it preserves only uncommitted edits without breaking restore-reproduces-snapshot; rides the conflict sidecar. - ๐ฅ Principals / roles / invites + write enforcement (M8a) โ the membership layer E2E + P2P both need underneath them.
- ๐ Read-side ACL + deny-by-default writes (M9) โ only meaningful once shares span multiple owners; the genuinely new attack surface, staged after write-side.
- ๐ข
snapshot_chunksedge table (M9) โ a derived-refcount source for E2E/P2P; deferred until a consumer exists (the migration runner makes adding it a one-liner).
๐ฎ v2 backlog
- ๐ค LAN peer chunk-exchange โ co-located nodes swap chunks directly (Syncthing-style)
- ๐๏ธ Interactive conflict resolver โ diff + keep-mine/theirs/both/edit
- ๐ Re-share / delegation (the
spermission) - ๐งฌ Content-level 3-way text merge
- ๐ Laptop power sanity โ pause-on-metered / pause-on-battery / sync windows
- ๐ Client-side E2E chunk encryption
- ๐ฐ Hub clustering / HA
- ๐ผ๏ธ Full TUI dashboard
๐ AGPLv3 core โ self-host free, forever. ย ยทย ๐ผ Commercial license for the hosted tier.
devbox is open-core: the entire hub + clients in this repo are AGPLv3 and fully
self-hostable. A future hosted version (signup, billing, provisioning) is a separate, closed
control plane that wraps the same single-tenant hub โ designed for via clean seams (BlobStore
interface, config-driven limits, /metrics) but not part of this OSS repo.
Offering devbox as a hosted service? AGPLv3 means open your changes โ or grab a commercial license. ๐ค
โ v1 is feature-complete โ the spec documents the full design, and
scripts/build-release.shcross-compiles every platform.
- ๐ด Fork & branch
- ๐งช Keep it lazy-correct (smallest diff that works, tests for non-trivial logic)
- ๐ Update the docs in the same PR โ they get the full flare treatment ๐
- ๐ Open a PR
