@polycode/secrets stores provider API keys in the most secure backend available:
- OS keychain (preferred) — via
@napi-rs/keyring. On Windows this is the Credential Manager, DPAPI-backed (encrypted at rest, scoped to your login). Verified active on the dev box. 0600file fallback —credentials.jsonin the user config dir (%APPDATA%\polycodeon Windows,~/.config/polycodeelsewhere) if the keychain module isn't available. Plaintext, but outside the repo and protected by user-profile ACLs / POSIX0600.
backendName() reports which is active; storeLocation() shows where.
When the app needs a key it checks, in order:
process.env(including a loaded.env)- OS keychain
- file store
hydrateEnv() copies stored keys into process.env (without overwriting existing values)
so the AI SDK provider factory — which reads env — can see them.
- Never echoed — the setup screen masks input (
•). - Never logged — keys are not printed; Settings shows only each provider's status/source, never the value.
- Never committed — keys live in the keychain or the user config dir, not the repo.
.envis git-ignored. - Never sent anywhere except the chosen provider's own API.
The agent gates every tool call by its permission class (safe | mutating |
dangerous) according to the active mode (/mode <...>):
| Mode | safe | mutating (write/edit) | dangerous (bash) |
|---|---|---|---|
plan |
allow | refuse | refuse |
ask (default) |
allow | prompt | prompt |
acceptEdits |
allow | allow | prompt |
yolo |
allow | allow | allow |
Deny rules and protected paths (.env / .git / .ssh / .polycode) run before the
safe-class short-circuit, so Read(.env) is refused even though read is safe. Use
yolo only in sandboxes / CI — the sandbox still jails those paths.
This workspace is private. There is no npm publish path. Ship the tsup bundle or the
Docker image from git.
MCP stdio servers must be a local command (node ./mcp/foo.js, a venv binary, …).
npx/npm/pnpm/yarn/bunx are rejected at connect so a checked-in mcp.json cannot
pull and execute a public package. HTTP MCP (url) is DNS-checked the same way as
web_fetch (no private/loopback/metadata) and still goes through the permission engine.
Tool outputs are scrubbed for sk- / xai- / sk-ant- / Google AIza / GitHub ghp_ /
Bearer tokens, *_API_KEY= assignments, and PEM private keys before they enter
model history. This is leak-prevention for both logs and training-tier vendors.
Tools never touch Node's fs/child_process directly — all I/O goes through a Sandbox
(in @polycode/core). File ops are path-jailed to the project root (.. traversal is
rejected). bash is classified dangerous and gated by the permission mode.
The Sandbox has two backends (@polycode/sandbox):
local(default) — host filesystem + host shell, path-jailed. Fine for local, trusted use.docker— shell commands run inside an isolated container; file ops operate on the bind-mounted project, so edits still land in your real files. The container is locked down:--network none(no egress),--cap-drop ALL,--security-opt no-new-privileges, plus--pids-limitand--memorycaps. The container is removed on exit.
Select it:
corepack pnpm dev -- --sandbox docker # or set "sandbox": { "kind": "docker" } in configIf kind is docker (or you pass --sandbox docker) and Docker isn't available, the CLI
fails closed — it does not silently fall back to local. Config knobs: image
(default node:22-alpine), network (default false), memory (default 1g).
File tools and sandbox.walk refuse .env / .env.* / .git / .ssh / .polycode
(except memory.md and reviews/). bash commands that name those paths are refused even
in yolo. Child processes (bash, MCP stdio, LSP, git worktrees, hooks, statusLine) get
process.env with *_KEY / *_TOKEN / *_SECRET / POLYCODE_AUTH_TOKEN* stripped.
Design note: the sandbox isolates the execution surface (arbitrary shell), which is the real risk. It does not isolate file edits from the project — that's intentional, so the agent's changes apply where you want them. For fully untrusted use where edits must be quarantined too, run on a disposable copy of the repo (or a git worktree).
GET /health and /ready stay public. Set "hosted": { "trustProxy": true } only behind
a trusted reverse proxy (X-Forwarded-For). Bind 127.0.0.1 locally; Docker images listen
on 0.0.0.0. poly --serve is not a yolo scaffold:
- Auth —
POLYCODE_AUTH_TOKEN(and optional comma-separatedPOLYCODE_AUTH_TOKENS), each at least 16 characters. Clients sendAuthorization: BearerorX-Api-Key. Compare is SHA-256 +timingSafeEqual.--insecureskips auth and forces yolo — local scaffold only, never company networks. - Bind — default
127.0.0.1. Binding a non-loopback address without--insecurerequires a docker sandbox (fail-closed)."profile": "company"forces docker + non-yolo. - Permissions — no interactive prompt.
askbecomesplan.yolois refused unless--insecure. Docker defaults toacceptEdits; local defaults toplan. - Limits — 1 MB body, 32k message, 5-minute turn abort, per-IP rate + concurrency. Failed auth counts toward the window.
- Audit —
.polycode/audit.jsonlrecordschat/tool/unauthorized/rate_limitedwithrequestId— no prompt or tool bodies. - Sessions — transcripts omit raw image bytes.
web_fetch and HTTP MCP URLs are lexical-checked then DNS-resolved; any private /
loopback / link-local / metadata address is refused. Redirects are not followed.
Hooks remain user-configured shell (trusted config). They run through the sandbox with
secret env stripped; a nonzero PreToolUse / UserPromptSubmit denies the call.