Security overview

Scoped amendment (2026-10-26; reviewed by Ian): The Admin page now accepts a signed-in owner's backup destination and recovery password, while the CLI and install runners perform captures. This reflects the worktree changes and is not a full security refresh.

Scoped amendment (2026-10-26; reviewed by Ian): Adds opt-in encrypted backup egress and its key-custody limit from the box-backups worktree. It is not a full security-report refresh.

Scoped amendment (2026-09-21), reviewed by Ian: Quick chat only, against 34eaa95fc3f9a48d29f4e2d32c6e8aab3e3de04b. The unchanged generated-at-rev remains the previous full-inventory anchor. Unrelated historical changes, the private security tier, and the complete surface map were not re-audited.

Scoped amendment (2026-10-06), reviewed by Ian: Quick chat only. The box screen on web and iOS replaces the Quick chat page; the server now stores, routes, and posts each thought, and an uncertain thought waits for the person. The Jev egress path is unchanged. This is not a full security-report refresh.

Scoped amendment (2026-09-24; DRAFT — unreviewed): Updates managed site publishing authority and Cloudflare credential custody against aa084d01ebdee12e2e88d210c2c5a5c85b7cc294 plus the uncommitted publish-pages worktree. This is not a full security-report refresh.

Scoped amendment (2026-09-25; DRAFT — unreviewed): Adds owner-only custom hostname assignment: DNS and certificate changes can begin while the site is disabled, and a signed-in member separately approves the destination. An uncertain attachment stays pending and blocks approval/enable. The preflight checks Worker Custom Domain assignments, not existing DNS records or Workers Routes, and a hostname reservation cannot be released from Bee Box, even after manual Cloudflare detach. HTTPS readiness and the least-privilege token permissions have not been live-tested; the full inventory anchor remains unchanged.

Scoped amendment (2026-09-26; DRAFT — unreviewed): Replaces per-publication hostname setup with one shared hostname and Worker per box. Pages in one box share browser origin, storage, and same-origin script access by explicit boxholder decision; CORS and iframes do not provide isolation. New public paths are /<slug>/ and secret-link paths are /s/<pubId>/. Existing per-publication URLs remain compatible, while account-restricted managed sites stay blocked. Cloudflare hostname permission requirements and live HTTPS readiness remain unverified.

Scoped amendment (2026-09-28), reviewed by Ian: Adds opt-in admitted-document triage and replay against 5844a1024. Unrelated security surfaces and the private tier were not re-audited; the full-inventory anchor remains unchanged.

beebox is a personal assistant that a Claude Code agent operates on your behalf: it reads your email, listens to your voice memos, edits your files, and runs shell commands. A system like that deserves a blunt security document, so this one leads with blast radius, not reassurance.

This is the readable overview. The reporting policy is SECURITY.md at the repo root; the full accounting is security-report.md.

This document is maintained by an agent, reviewed by a human. The process that generates it — an ordered inventory and evaluation rubric — is committed at .claude/skills/security-report/SKILL.md (repo root), and the full accounting it produces is security-report.md: every endpoint and its auth, every credential and its blast radius, every place data leaves the machine. You can't verify a security doc wasn't shaped by error or malice, but you can read the rubric that produced it, the diff of every regeneration, and the review header above. That's the claim this document makes: it is auditable as a process, honest about where it isn't verifiable as an artifact.

Reporting a vulnerability

See SECURITY.md — the reporting policy lives there so it is where GitHub and a first-time reader look for it.

Threat model, briefly

beebox assumes a single trusted operator (plus, optionally, a few invited members they personally trust). It defends the box from the network — authentication is structurally always-on; there is no flag, env var, or config field that disables the login wall — and it limits what the box's own moving parts inherit: per-box processes and agent subprocesses get an allowlisted environment that omits the session-signing secret and other cross-box credentials (two known exceptions are open: box card schemas load inside the server process, and server-side commits run box Git hooks with the server's environment). Be precise about what that is: env-level isolation, not OS-level. Everything runs as one OS user, and the secret is also a 0600 file that same-user code could read — the stripping stops accidents and lazy exfiltration, not a determined same-user process. It does not defend you from your own agent: the agent is the product, and it runs with real power (next section). It also does not currently treat invited members as adversaries — membership grants broad capability short of admin operations (details).

What the agent can do

Plainly: the box agent runs Claude Code with bypassPermissions and no tool allowlist. It can execute arbitrary shell commands as the user the box runs as, and read or write any file in the box. Its working scope is the box directory, and no current call site widens it beyond that — but the scope parameter itself is unguarded caller input, and either way it is a convention the agent operates within, not a sandbox that contains it. Treat "what can the agent do" and "what can beebox do" as the same question. On a server or in the Docker image it can also install distro packages as root through one validating wrapper, which allows only additive installs from the distro sources and refuses packages that add services, root jobs, privilege grants or setuid files; the packages' install scripts still run as root. On fresh boxes, the connector syncs (email, calendar, Drive) and chat review are off by default, so no schedule pulls in outside content until you turn it on. Four maintenance schedules are on: map refresh, run cleanup, the weekly retrospective and the daily todo review. The last two run an agent over content already in the box, and only when a chat session has gone quiet or a todo needs review.

Prompt injection — the risk we most want you to understand

beebox is, by design, an agent that reads your private data, ingests untrusted external content, and acts with no tool allowlist. Those three together are the well-known "lethal trifecta": text written by someone else — an email body, a web clipping, a calendar invite, even words inside a photographed image — can reach the agent's context and try to steer it. If that succeeds, the attacker isn't limited to reading one card; they have whatever the agent has, which is arbitrary shell as your box's user.

We're telling you this plainly because the honest mitigations today are thin. There is no injection filter and no containment sandbox. What actually reduces the risk is the shape of how you run it: it's your own single-operator box (the blast radius is your data, not a stranger's), scheduled processing is off until you enable it, and account credentials are managed outside the box agent's API. A signed-in box member approves a site's first enable and any audience or destination change; after that the agent may publish content updates within the approved scope. This is a real permission boundary at the app API, not a sandbox against hostile code running as the same OS user. Tighter containment is tracked and not yet built. Until it is, be deliberate about which untrusted sources you connect, and don't leave the agent processing them unattended in a box that can reach anything you'd mind losing.

What leaves your machine

The complete inventory is §3 of the structured report. The summary:

One caveat worth naming: the iOS app's dictation prefers Apple's on-device recognizer, but on older systems it falls back to Apple's cloud speech service without an app-level opt-out (issue).

The authentication surface

Every box route — HTTP and the WebSocket upgrade alike — sits behind one auth wall. A request gets in with a logged-in session (local password, scrypt-hashed, throttled; or Google OAuth), a scoped machine credential (mobile device token, per-box agent token, scan token), or not at all. Box access fails closed: a box with no explicit member list is owner-only. A corrupt credential store answers 503, never "logged out."

The deliberately unauthenticated surface is small enough to list: the CSP violation report sink (spec-required, tightly capped), a build-info probe (a hash and a flag), the login/static assets needed to reach the login page, and — for its 15-minute first-run window — the setup route, gated by a token printed only to the server console. Everything else that skips the session wall carries its own dedicated credential (the diagnostic bearer key). The full route-by-route table is §1 of the structured report.

Publishing

Managed static-site publishing deliberately exposes selected box content. Each box has one Admin-configured hostname and shared Worker; public paths use /<slug>/ and secret-link paths use /s/<pubId>/. Before first enablement or a scope change, a signed-in member reviews the requested audience, destination, file summary, and leak-scan findings on the publication card in the app. That grant permits subsequent content updates in the same scope without per-snapshot approval. The agent can edit the card's request, build, prepare, and read back published files; a changed audience or destination cannot go live until a member approves it. The boxholder explicitly accepts that pages published by one box share browser origin, storage, and same-origin script access. CORS does not separate those pages; no iframe or per-publication origin isolation is provided. Treat publications in one box as mutually trusting. DNS/certificate effects begin when the owner configures the host, before any page is enabled. Static sites render Markdown to HTML at prepare time, reject box component tags, and omit redacted content; the leak scan runs over that rendered output. Leak scanning is a backstop, not a guarantee that content is appropriate or free of secrets. The older draft/go publication flow, its submission intake, and its separate R2 ingestion credential no longer exist; the Worker is read-only. Public and secret-link bundles are fully public to anyone with the URL; a secret URL is a bearer capability, not a login. Managed account-restricted publication is not ready pending a separate consent and security design.

Known limitations and accepted risks

The full register with rationale is §8 of the structured report. The ones you should actually weigh:

Known gaps (tracked, not yet accepted or fixed) live in the issue queue — at this writing they include plain-HTTP between Cloudflare's edge and the origin on the public deploy path, deploy-time infra config drift, and how much capability an invited member should hold. (The connector secret-file permission gap this list used to carry is closed: connector credentials now live in the machine secret store, not box files.) The structured report lists each with a pointer.

A few gaps are tracked privately rather than in the public queue: where a defect is specific and unpatched enough that publishing its exact location would be a roadmap to a live hole, we hold it until it's fixed and name it only by class here. That's a deliberate rule, not concealment of the kind of problem — the structured report says which category is affected, just not the file:line. It is the mirror of the prompt-injection section above: architectural risks we disclose loudly; location-precise unpatched defects we disclose once they're closed.