Locus is a local control plane. Treat it like a hypervisor plus an LLM keyring, not like a multi-tenant SaaS.
Please use GitHub private vulnerability reporting on this repository. Do not file a public issue for a guest escape, token leak, or unauthenticated control-plane access.
- Bind the HTTP/WebSocket control plane to loopback (
127.0.0.1/::1/localhostonly) unless Settings → Phone is enabled. Saving a non-loopback bind host in Advanced is still rejected. Phone remote binds0.0.0.0and requires a vaulted pairing token for every non-loopback client; public internet peers are rejected even with a token. The same port multiplexes loopback HTTP with LAN HTTPS (self-signed cert in~/.locus/tls/, fingerprint on the pairing QR). Minting, rotating, and revealing that token is loopback-only. Restart after disabling phone remote so 7420 leaves0.0.0.0. ThelocusCLI (apps/cli) is another loopback client of that API — same bind, redaction, and approval gates as the desktop. - Reject browser requests whose
Originis not loopback. - Keep QEMU display, SSH, RDP, WinRM, QMP, and serial forwards on loopback. VNC has no password and SPICE uses
disable-ticketing; that is accepted for a single-operator loopback display, not as a network service. - Talk to LAN
locus-workerhosts over HTTPS and pin the worker certificate fingerprint. Test connection observes the fingerprint; paste it into TLS fingerprint after matching the worker printout, then Save. Non-loopback workers require that pin on first save (no silent TOFU). WebSockets refuse to connect until a pin is saved. Cleartext worker URLs are rejected except loopback. Workerruntime:"host"(tools assh -lcon the worker box) is off untilLOCUS_WORKER_ALLOW_HOST=1. Worker QEMU guests keep VNC/SPICE on the worker loopback; the laptop reaches VNC through a pinned WSS proxy (/v1/guests/:id/display), never by publishing VNC. Cursor / Claude / Codex / Kiro on Linux or Windows VMs spawn over SSH with-Rof the control-plane MCP port into the guest (PowerShell on Windows). The worker token is printed only on a TTY the first time it is created. UnauthenticatedGET /healthreturns{ok,service,protocol}only. - Redact provider keys, forge tokens, guest passwords, and matching vault secrets in API responses, traces, session JSON, and tool output sent to the model. Tool-argument values are pattern-scanned, not only secret-named keys. GitLab
glpat-, phoneph_, Chrome extensioncx_, and AtlassianATATTformats are included.GET /api/automationsmaskswebhookSecret/slackSigningSecret; create/rotate still returns the secret once. - Store those secrets in
~/.locus/secrets.enc(AES-256-GCM). The packaged app wraps the master key with ElectronsafeStoragewhen the OS keychain is available, then deletes plaintextmaster.key. - MCP stdio servers inherit a minimal host env (
PATH, locale, temp). They do not receiveLOCUS_MASTER_KEY. - Generate a random guest password on first run. Ubuntu autoinstall identity uses a SHA-512 crypt of that password (not a hardcoded
locushash). Settings → Machines can rotate it (applies to the next install, not an existing disk). - Gate
git_push,git_pr, and MCP tools from the agent loop on explicit Approve / Deny in chat, unless the session or default autonomy tier auto-allows them. MCP tools allowlisted on an automation skip that gate for that job only. Supervised asks for every mutating command. Guided (default) asks for push, PR, MCP, plugins, and package installs and has no default USD cap. Host-runtime Guided also asks forexec/powershell/start_process(those tools can reach the rest of this machine). Autonomous auto-allows push, PR, MCP, plugins, and installs, and requires a spend cap (default $20); protected default branches still wait. - Guest network jail defaults to restricted (forge + approved provider allowlist). VMs use
restrict=on; Docker uses an HTTP(S) CONNECT proxy plus iptables OUTPUT DROP except loopback, Docker DNS (127.0.0.11), and the host proxy. CONNECT allow/deny is logged asegresswith a byte count. Local loopback proxies (QEMU10.0.2.2/ Docker on this machine) stay unauthenticated. Worker proxies bind0.0.0.0sohost.docker.internalcan reach them and require proxy basic auth; do not publish the ephemeral proxy port. An admin managed policy (/etc/locus/policy.jsonor signed~/.locus/policy.json) can force this jail, allow-list providers, and disable Host agents / MCP / automations, Use my Chrome, phone remote, Autonomous, and Giphy lookup. Details: docs/managed-policy.md. - Host web search (
websearch) is off until the operator enables it per agent. It fetches public search results from the host (DuckDuckGo / Wikipedia only) and bypasses the guest network jail. Results are wrapped as nonce-tagged<untrusted-data>so the model must treat them as data. Cursor CLI sessions use the same composer toggle: off strips Cursor's built-inwebSearch/webFetchviaagent --exclude-tools; on leaves those tools in Cursor's catalog. Review Cursor CLI sessions always strip write/edit/shell so the reviewer cannot patch the branch. - Wrap untrusted content (webhooks, web search, MCP / forge comments, operator load-path notes) in nonce-tagged fences. Tool output from outside the workspace is hostile by default.
- Refuse
file_read/grep/glob/file_listof credential paths (.env, cloud keys, SSH private keys).grepwithout a path still drops hits on those leaves. Known secret formats are redacted from traces and tool output.exec/powershellthat dump process environment (printenv,env,/proc/*/environ,export -p) are refused. - Gate
npm/pip/npx/apt/brew/ similar installs,npm runlifecycle scripts, andpackage.jsonlifecycle scripts (postinstall, …) on Approve / Deny unless--ignore-scriptsis used. The same gate applies topowershell. - Rate-limit failed phone-token and webhook-secret checks (20 / minute / peer). Debug traces (
/api/debug/logs*) are loopback-only. Worker probes (POST /api/workers/test) and indexing an arbitrary Brain path (POST /api/brain/buildwithpath) are desktop/loopback-only. - Log restricted-jail CONNECT allow/deny as
egressevents in~/.locus/traces.
- Restricted jail is default-deny egress through a logged allowlist. Docker guests still do not keep
NET_ADMINon the long-lived container (iptables is applied via a privileged-exec helper). Missing iptables in a restricted guest fails the start instead of silently falling back to proxy env. QEMU user-netrestrict=onis still the VM equivalent, not a hypervisor firewall. - Direct tool invocation from the guest pane is an operator action and does not go through chat approval.
- Host MCP is the security exception: those servers run on the operator machine. Guest MCP (stdio) runs inside the Docker container. HTTP/SSE MCP stays on the host. Only add servers you trust.
- Host automations (and host agents) run tools on the operator machine with no VM or container.
file_*/apply_patchstay inside that session's/workspace;execand git can reach the rest of the host. Treat that the same way as host MCP: opt in only when you want the job on this box. - Forge webhooks (and Slack / generic HTTP webhooks) are reachable only if something can POST to loopback or to a Tailscale Funnel that proxies only
/api/hooks. An Amazon API Gateway HTTP integration may use the same/api/hookspath; set the webhook base URL to that origin. Locus unwraps REST/HTTP API proxy events. GitHub.com and Slack.com cannot reach127.0.0.1. GenericX-Locus-Secretorx-api-keyrequests also needX-Locus-Timestamp(unix seconds, ±5 minutes) as a real HTTP header. A timestamp only inside an API Gateway JSON envelope is ignored, so Funnel/API Gateway integrations must forward that header. GitHubX-Hub-Signature-256stays body-HMAC. Slack signing secrets can be set per automation (else Settings → Advanced). A leaked secret on a forwarded port would still be enough to queue a run within the timestamp window. Webhook bodies are wrapped as a nonce-tagged<untrusted-webhook-event …>block, size-capped (bytes, streamed), and must not be followed as instructions. First-party Jira/Confluence/Linear/Slack/Grafana/Miro/Figma/incident.io/AWS IAM/Portainer/Gmail/Calendar/Drive/Kubernetes/Sentry tools are off on automations unless listed on that job's allowlist. - First-party Jira / Confluence / Linear / Slack / Grafana / Miro / Figma / incident.io / AWS IAM / Portainer / Gmail / Google Calendar / Google Drive / Sentry / Kubernetes tools run on the host with vaulted tokens, like forge PR tools. They are omitted from the agent catalog until Settings → Integrations has a token for that product (AWS IAM also becomes ready when instance metadata is enabled; Kubernetes becomes ready with a token or a loopback
kubectl proxyURL). Mutating Jira comments, creates, updates, transitions, Confluence pages, Linear comments/creates/updates, Slack posts, incident.io creates/updates, Miro export, Figma comments, Portainer start/stop, Gmail sends, Calendar creates, Drive uploads, and Sentry updates, Kubernetes apply/delete/scale wait for Approve / Deny. AWS IAM tools in this slice are reads (GetCallerIdentity, list/get/simulate). Kubernetesk8s_getof Secret objects redactsdata/stringData. Cursor CLI sessions get the same tools through a loopback MCP server (POST /mcp);agent --approve-mcpsis the operator opt-in for those calls. Claude Code sessions get the same server via project.mcp.json(type: http) and--mcp-config .mcp.json. The MCP config file stores only the loopback URL, not tokens. Worker Cursor CLI and Claude Code use a loopback proxy on the worker host that tunnels JSON-RPC back to this control plane — it does not bind the laptop MCP port on the LAN. AWS IAM/STS calls use signedfetchon the host (no AWS SDK), same as Fargate placement. Optional IAM/STS/ECS endpoint URLs and an AWS gateway URL keep their path;execute-apihosts sign asexecute-api. HTTP MCP and OpenAI-compatible Bedrock / API Gateway URLs sign the same way when AWS keys are configured. The control plane bind host stays loopback. Instance metadata is IMDSv2 or the ECS credentials URI;AWS_CONTAINER_CREDENTIALS_FULL_URImust be link-local or loopback. Kubernetes tools talk to the API server (or loopback kubectl proxy) from the host; they do not run kubectl in the guest. - Integration plugins are the same host-side HTTP exception, authored by
manage_integration_plugin(Approve / Deny). Secrets belong in Settings → Integrations, not in the manage tool. Updating a plugin keeps vaulted secrets; changing a secret field to a non-secret type drops the value instead of writing it toconfig.json. The request host must come from a plugin URL field or a hardcoded https host; argument values cannot retarget the host. Redirects are refused. Plugin responses are nonce-fenced. GET tools withwrite: falseskip Approve / Deny (they are treated as reads); non-GET tools always wait. Automations only see listedplugin_*names. - Slack identity. Messages Locus sends default to you (composer or approved
slack_post). Passas="bot"or use the composer toggle to post as the Locus app when a bot token is stored. The Slack app is OAuth + Socket Mode plumbing. Socket Mode is outbound WSS; Slack.com still cannot reach loopback. Funnel stays/api/hooksonly. Slack action buttons may Approve / Denyslack_postandslack_uploadonly —git_pushand other forge writes stay in Locus. Thread excerpts are nonce-fenced (kind="slack-message"). The cache under~/.locus/slack/stays on this machine and is encrypted with the vault master key. Slack file GIFs and videos are fetched with the user token through/api/slack/media(Slack file hosts only). HTML and CSV files stayContent-Disposition: attachmenton that route unlesspreview=1, which serves HTML with a strict CSP for asandbox=""iframe (no scripts) and CSV as inline text for the table preview. Video is served inline for the in-pane player. GIF search is outbound to api.giphy.com whenLOCUS_GIPHY_API_KEYis set and managed policy allows it; otherwise a local catalog. - Host web search also runs on the operator machine. Leave it off unless you want that agent to query the public web.
- Use my Chrome is a second Host-runtime exception: an unpacked Chrome/Edge extension on loopback can drive the operator's real tabs (cookies and logins included) for Host agents that opt in. Isolated
host-chromestays the default. Pairing tokens (cx_) are vaulted like phoneph_. The websocket is loopback-only; navigation is an http(s) allowlist (file:,data:,chrome://, and other schemes are refused). Workers cannot use live Chrome. - Android pairing loads HTTPS only (cleartext disabled). The TLS fingerprint is required; a
locus://pairdeep link prefills the form and asks for confirmation before connecting. The token is stored as a cookie for the WebView, not appended to the HTTPS URL. Third-party cookies are off. - Electron keeps
contextIsolation/sandbox/nodeIntegration=false, injects the control-plane CSP, and cancelswill-navigate/will-redirectaway from loopback.connect-srcis loopback WebSocket hosts, notws:/wss:wildcards. - New installs get a random guest password, including builtin templates merged into an existing config. Older configs may still use
locus/Locus!dev1until you rotate them. Anyone who can already talk to guest SSH on loopback can log in with those credentials. - Secrets live in
~/.locus/secrets.enc. The master key is~/.locus/master.key(mode 0600) or ElectronsafeStorage(master.key.safe). The packaged app removesmaster.keyafter wrapping. A non-hexLOCUS_MASTER_KEY(a passphrase) is stretched with scrypt and a per-install salt in~/.locus/master.salt(mode 0600); existing passphrase vaults migrate off the legacy static salt on the next successful decrypt. Disks, golden images, and recordings live under the same tree. Back that directory up like you would SSH keys. Settings → Advanced → Reset all data deletes this tree after two confirms. - The mock hypervisor is for UI development. It does not isolate workloads.
- Never put secrets in the prompt. Provider keys, forge tokens, and guest passwords live in
secrets.enc. Git tokens are injected asGIT_TOKEN(and friends) at container/VM start. Guest git uses a credential helper that reads$GIT_TOKEN— the token is not placed on argv viahttp.extraHeader. Docker run/exec pass vaulted env through a 0600--env-file, not-e KEY=value. Cursor CLI usesCURSOR_API_KEYin the child env, not--api-key. Settings → About states this policy in the UI. Guest cloud-init / Omarchy identity uses a SHA-512 crypt of the vaulted password (notplain_text_passwd, and Omarchy does not fall back tolocus). Windows Autounattend still needs a plaintext password in XML (the format requires it); that file is mode 0600. - What goes to providers. Chat messages, tool results (after secret redaction), and screenshots when Vision is on are sent to the provider you enabled. Locus does not send traces, vault files, or
master.key. - Training. Locus does not train models. Whether a provider trains on API traffic is that provider's policy; use your provider's zero-retention / API-data controls when you need them.
- Retention. Session JSON, traces, screenshots, recordings, and disks stay under
~/.locuson this machine until you delete the session or Reset all data. Automations that open a new chat per run keep the newest 60 chats for that job (pinned and in-flight chats stay) and delete the rest from this machine. There is no cloud copy of the control plane. - Isolation. This is a single-operator local app. Each agent has its own guest (Docker workspace or VM disk). That is workspace isolation, not multi-tenant SaaS tenancy. Do not run untrusted operators against one
~/.locus.
- A turn snapshots the guest (or
/workspace) before the user message is appended. Undo / rewind restores that checkpoint so cancelling a mid-edit agent leaves a consistent tree. - Completed tool results are persisted in the session JSON as they finish. If the control plane dies mid-turn, the next boot parks
runningsessions and appends a recovery note; send a message to continue. Replaying the same tool-call id skips work already done. - Compaction summarize-and-prunes old turns but re-injects the session plan, task_list, and scratchpad (and an explicit in-progress list) so a fold cannot drop the plan mid-surgery.
- Diagnosable tool failures trigger a self-correct nudge (read the output, form a hypothesis, retry) instead of stopping for the operator. Cap is two nudges per turn. Traces record
selfCorrectplus approval waits onturn.end. - Every turn is JSONL under
~/.locus/traces/. Failed turns also write~/.locus/evals/*.jsonso a regression from last Tuesday is a fixture, not a story. The agent ⋯ menu → View trace can copy or save that agent’s JSONL as an eval, and shows an egress summary plus an approvals export (GET /api/sessions/:id/audit/egress|approvals, loopback-only like/api/debug/logs*). - Model catalogs use exact ids, not
*-latest. Settings → Providers → Test connection probes the adapter. Compare models A/B-tests a prompt on a second model before you blame the harness. A dailyharness-smokeworkflow re-runs the wrapping, redaction, supply-chain, and prompt-variant tests.
- Do not expose port 7420 (or guest forwards) on the public internet. Tailscale Funnel, if used, must proxy only
/api/hooksto127.0.0.1. Phone remote is LAN/Tailscale plus a pairing token — still not a Funnel of the control plane. The Chrome extension talks only to loopback. Keep Settings → Advanced bind host on loopback. - Guests start restricted. Keep none for the tightest guests; Open is only for first-boot apt unless a managed policy forbids it.
- Keep forge tokens and provider keys in Settings; do not paste them into chat. The agent cannot read
.envor cloud credential files into context. - Review Approve/Deny for push, PR, MCP, Jira comments/creates/transitions, Linear comments/creates/updates, Slack posts, Sentry updates, Kubernetes apply/delete/scale, integration plugins, and package installs that can run postinstall scripts — or set Autonomy to Supervised if you want every mutating command, or Autonomous (with a spend cap; default $20) for unattended CI. Autonomous also auto-allows MCP, plugins, and installs. Host-runtime Guided also waits on exec. Guided/Supervised have no default USD cap. Automation MCP / integration allowlists are an explicit operator opt-in. Stop / take over / rewind cancels a pending gate.
- Treat automations that start a VM as able to spend CPU, disk, and API tokens until you pause them.
- To sign macOS dmgs, set repo secrets
CSC_LINK(base64 p12) andCSC_KEY_PASSWORD. Unsigned builds still ship.