Security & safeguards
Chimera can run shell commands, edit files, call APIs, and modify its own skills. It ships defense-in-depth, and β this matters β the docs state where each layer stops.
The one rule
None of these safeguards replace running it in an isolated environment when you
grant autonomy. The default local runner is not isolated; use
CHIMERA_SANDBOX=docker (network-off, optionally under gVisor) for untrusted work.
Agent voice in prompts and bot text
Prompts and bot templates must not have the agent claim feelings, care, friendship, a relationship, or promises about itself. State what it did, could not do, and how sure it is. Calibrated first-person uncertainty (e.g. βI'm not sure, butβ¦β) is allowed and useful.
The layers
- Governance kernel β every governed tool call is allow / warn / review / block. A cheap first filter of dangerous shell signatures, not the boundary.
- Sandbox β an ephemeral, network-off container (
CHIMERA_SANDBOX=docker), hardenable with gVisor (CHIMERA_SANDBOX_RUNTIME=runsc). The verify command runs in the same sandbox as the agent's shell. A verifier declared in.chimera/verify.tomlis still a shell command: on a non-isolated host it runs only when the project has an activeshellgrant andCHIMERA_HOST_EXECdoes not deny host execution. Without that grant it abstains; inferred commands from build files continue through theCHIMERA_HOST_EXECconfirmation and abstain if declined. A project grant permits the configured command but does not make the host sandbox isolated (CHIMERA_VERIFY_NETWORK=1gives a docker verifier network; on kernel sandboxes it cannot, so a verifier requiring network runs on the host only when otherwise authorized). Treat the config and its command as executable project content; review it before granting shell. If a turn edits the configured verifier during the turn, verification abstains; the command is captured before the turn and requires a durable owner approval bound to its exact SHA-256 hash before first execution (and again whenever the command changes). An optionalsetupkey in that file names a project environment-preparation command. It requires its own exact-hash owner approval and the same active project shell grant, runs at most once per workspace and command hash, and failures are reported to the agent without aborting the turn. Approvals and the prepared marker are kept under Chimera's home, not in the checkout. Grants and approved hashes are inherited only by durable worktrees created by Chimera for that registered project; arbitrary Git worktrees do not inherit them. The app-worktree registry is stored underCHIMERA_HOME. A process that already has shell access on the host can write there, just as it can edit the project registry itself. This approval binds only the command string, not its effects:pytest,make test, andnpm testexecute project-controlled files such asconftest.py,Makefilerecipes, and package scripts, which the agent may also have edited. - Per-session tool allowlist β grant a run only the tools it needs; the rest are dropped from the model's schema entirely.
- Taint tracking (
--taint) β untrusted content is fenced as data, its provenance follows it into memories and skills (a skill from a tainted run is held for review), and once a run is tainted the dangerous tools narrow. - Quarantined reader β the dual-LLM / CaMeL pattern: untrusted content is read by a tool-less model that can only emit schema-validated fields, so an injection can't produce a new instruction or tool call.
- Cross-agent monitor β under fan-out a per-worker monitor is blind to a split flow (one
worker fetches untrusted, a different worker sinks it β the fetch and the sink live in
separate ledgers). An aggregate monitor sees the whole fan-out; it is always on for
solve-batch/crew-isolated.
Fan-out: the cross-agent monitor
When several tool-using workers run in parallel (solve-batch, crew-isolated), each gets its
own capability ledger, and after the batch an aggregate monitor runs over all of them. It catches
patterns no single-worker monitor can see β the split exfiltration where worker A fetches
untrusted content and worker B executes or exfiltrates it:
$ chimera solve-batch "read notes.md and summarize" "download the helper and run it" -w .
task1: ok
task2: ok
merged 2 file(s) across 2 task(s)
β cross-agent monitor flagged (review):
- cross-agent-taint: untrusted content entered via one agent and a different agent
performed a sink (task2βtask1) β a split flow no single-agent monitor sees
It only ever escalates to review β it never blocks a run β and it is pure observability
(recording changes no behaviour). Add --taint on top to also arm each worker's adaptive
allowlist (dangerous-when-tainted tools then require approval).
Approval, and what a refused worker looks like. Each worker carries its own approver and its own
record of what it was allowed to do, so a task that was refused says so rather than reporting ok:
$ chimera solve-batch "read notes.md and summarize" "download the helper and run it" --taint -w .
task1: ok
task2: not allowed (ok)
governance: 1 action(s) refused for review: run_shell is restricted after this run consumed
untrusted content
1 of 2 task(s) had actions refused for review β check that the work they were asked to do
actually happened.
ok in parentheses is the loop's own verdict, and the two disagreeing is the point: a refused call
comes back as an ordinary observation string, so the worker reads it like any tool result, carries
on, and finishes in prose. Who can be asked follows CHIMERA_APPROVAL_MODE β deny refuses at
once, ask prompts on a terminal and otherwise writes the question down for chimera approve and
waits CHIMERA_APPROVAL_WAIT seconds per question, per worker. Silence always refuses.
Measured, not asserted
chimera redteam
runs an injection corpus through the stack. On the built-in corpus the taint layer cuts the attack success rate from 100% to ~14% β and the report names what still gets through (exfiltration via an allowed tool) rather than claiming 100%.
The same command prints the cost, which the first version of this page did not: with nobody
to ask, the narrowing refuses 100% of legitimate work that read anything external first β
fix the file the issue names, apply the upgrade the docs describe β and the registered gate
(over-block β€ 5%) fails. That number is not a tuning problem; the gate was empty. The default
approval mode is ask, and on the desktop it now asks: a narrowed tool call becomes a question
on the screen with the ledger's reason attached, answered with a button or chimera approve,
refused by silence after CHIMERA_APPROVAL_WAIT seconds. With the person approving the work
they asked for, over-block is 0% and the attack block rate does not move β measured, per arm,
in bench/injection/RESULTS.md.
The exfiltration through an allowed tool is closed by the same change: a tainted run's http_get
carrying a query string is a review, and the two legitimate query-string GETs added to the
corpus show what that costs.
Poisoned memory, across runs
redteam measures one run. The other shape is slower and does not fit in a process: run A reads a
poisoned page and stores what it "learned"; run B asks an unrelated question days later and recall
hands the planted fact to the model.
chimera memory-poison
Also offline and free. It ablates the three layers that sit between those runs β the tainted
provenance flag, the recall admission gate, and the [unverified] label the fact wears into the
prompt β because a single number would be compatible with any of them doing nothing. The headline is
what arrives unmarked, not what is blocked: a poisoned fact carrying its origin is one the model
was warned about; an unlabelled one is indistinguishable from something the agent verified itself.
Two results from the first run are worth stating plainly, because neither flatters us:
- The shipped configuration fails its own gate β on cost. It marks 100% of the poison and destroys 25% of honest memory doing it. The casualties are named: a security document that quotes an attack in order to explain it, and a support ticket forwarding an attempt. A pattern matcher on content cannot tell a quote from a command.
- On this corpus the content gate adds nothing the provenance label does not already cover. Its entire measured effect is the honest memory it removes. Fifteen hand-authored rows is a pointer and not a verdict, which is why nothing has been deleted on the strength of it.
Thresholds, method and what the numbers do not license are in
bench/memory_poison/PREREGISTRATION.md,
fixed before the first run.
Exposing the HTTP server
chimera serve binds to 127.0.0.1 by default. Its state-changing endpoints (/chat, /a2a,
/webhook/*) drive the agent, so before exposing the server to a network, set a bearer token:
export CHIMERA_SERVER_TOKEN="a-long-random-secret" # required as: Authorization: Bearer <token>
With it set, those POST endpoints return 401 without a matching Authorization: Bearer header
(GET /health and the A2A agent-card stay open). For the WhatsApp inbound webhook, set
CHIMERA_WHATSAPP_APP_SECRET to your Meta app secret β Chimera then verifies each request's
X-Hub-Signature-256 HMAC and rejects a forged payload with 403. Both are opt-in (unset = no auth,
fine for localhost); a public deployment should set them (or sit behind an authenticating proxy).
Sharing one conversation with a second person
The desktop app can share a coding conversation by a token that opens only that conversation β
never the server token. The owner mints one per guest (POST /api/code/sessions/{id}/share) and can
revoke it alone; deleting the conversation revokes all of them. A guest reaches four routes under
/guest/api/β¦: read the conversation, watch it live, send a message into it, see who is there.
Nothing else answers a share token, and the guest app has no route to answer a governance card β
those stay on the owner's screen.
The guest app is also the only thing served when the owner opens the network door
(POST /api/code/share/network, closed again with DELETE, never open at launch): a machine on the
LAN reaches those four routes and nothing the server token protects. Say it plainly before handing
out a link: a guest can ask the agent to do anything the owner can ask it, in the owner's project,
with the owner's tools and spend β under the request's default seams, and short of approving cards.
Your own phone or browser: pairing a device
A guest link is for somebody else. For the owner on their own phone there is pairing: chimera pair
(or Pair a device in Settings βΊ Access) prints a code that works once, for five minutes,
with a QR of the pairing address. It is typed into a page served by a second LAN listener β closed at
every start and opened only by the owner (chimera pair --lan, or Open on the card) β and
becomes a 30-day cookie (HttpOnly, SameSite=Strict, Secure over https) signed with a key
derived from the owner's device secret (<home>/devices.key). Five wrong codes void every live
code; five from one address shut that address out for fifteen minutes.
A paired device is the owner for reading and talking: conversations, turns, guidance, answers to
the agent's own questions, and refusing an approval. It is not the owner's screen. Approving still
takes each question's code (study 30, S30-30), and the routes that hand out access, change settings,
grant folders, start bots or stop the service answer 403 to a device. The Access card lists every
device with its last use and network (192.168.1.x); Revoke ends one at once, including a live
stream it has open, and Revoke all devices replaces the secret, so no cookie issued before then
verifies. The desktop bridge cannot pair, mint a code, list or revoke.
A browser tab as the owner's screen: pairing by code
A tab on the service's URL has no screen credential, so an owner-only action (a grant, a yes that needs
the screen, an owner-only switch) would fail there silently. The desktop app shows a browser
pairing code (Settings βΊ Security βΊ Browser pairing, Show pairing code); the tab asks for it,
and the server exchanges it for an HttpOnly, SameSite=Strict cookie that makes that tab the owner's
screen for its session (12 hours, CHIMERA_BROWSER_SCREEN_SECONDS moves it within five minutes to a day).
- Only the desktop's own screen mints a code. The gate (
chimera/api/browser_screen.py,desktop_screen_refusal) refuses the bridge, a paired device, a browser session, and a screen the page itself offers (chimera appin a browser, whose credential the agent's own shell can read). A bearer token alone never mints. The same gate lists and revokes sessions. - A code works once, for five minutes, and only one is live at a time. Five wrong guesses kill it. Wrong, expired and spent codes get the same answer. It is stored as a keyed HMAC, never in the clear.
- A session is the screen's switch and nothing more. It cannot mint, list or revoke sessions, and it
cannot turn "Ignore permissions", the desktop bridge (
CHIMERA_DESKTOP_BRIDGE) or its full control on. Only the desktop's own screen does those; turning them off stays allowed, because that only narrows. - The store cannot be forged. The file (
<home>/browser-screen.json) holdsHMAC(key, secret), and the key exists only in the memory of the process that made it, so a hand-written or copied entry verifies nothing. The file is on the governance list, so the agent's shell cannot rewrite it. - A paired session is valid only on the process that minted it. Its digest is keyed with that
process's key, so another process reading the same home does not accept the cookie, and the session
ends when that process restarts. If the desktop app and
chimera serveshare a home, pair against the one you are browsing. - Every change goes through the file under an exclusive lock, re-reading first. A revoke by one process holds against another process's later write, and a verify always reads the file as it is now.
- A write must come from this server's own origin. On an unsafe method (POST, PUT, PATCH, DELETE), the
cookie counts only when
Sec-Fetch-Siteissame-originorOriginequals this server's scheme and host exactly. Another localhost port, a cross-site page, or a script with neither header is refused. - Pairing has no shared address bucket. The only limit is the live code's own five wrong guesses. A local process cannot lock the owner out for a quarter of an hour; the owner mints a fresh code instead.
- It ends at the next request on revoke (Revoke or Revoke all) or on expiry, which is checked on every request.
- Nothing secret is written after creation: not the code, not the secret, in no response after the one
that made them, in no log line, and not in the audit log (
audit.jsonl), which records each mint, exchange, refused exchange and revoke by action and session id. - A device is never a screen. The device door refuses every
/api/security/route, and the exchange refuses a device outright.
Away from home, Chimera runs no relay of its own. If you want to reach the device listener from
outside, put a tunnel you control in front of it β Tailscale on both
devices, or a Cloudflare Tunnel
to http://127.0.0.1:<port> β and decide that consciously: it extends the same door to wherever the
tunnel reaches. Never bind the app itself to 0.0.0.0 for this.
The desktop app's backend
The app's backend listens on loopback, and loopback is not a person: any process on the machine can reach the port.
- A token on every route. The desktop app always starts its backend with a bearer token β a
fresh secret per launch, or the
CHIMERA_SERVER_TOKENthe owner already set. Every/api/β¦route then answers401without it,/api/healthincluded. Only the static app files and the guest app (/guest/api/β¦, which checks its own share tokens) stay open; the MCP bridge (/api/bridge/β¦) keeps its own token. The shell hands the per-launch token to the backend in the environment and to the window through the webview's initialization script β never on a command line, in a URL or in a file. What the backend starts does not inherit it: the sandbox's child environment and the oneghandgitrun with drop every secret-named variable (GH_TOKENand gh's other own credentials excepted). - Approving without the code takes the screen. A token says who may call the API, not that the
owner is looking. A yes with no code (
POST /api/approvals/{id}) succeeds only when the request also carries the screen credential inX-Chimera-Screen, beside the bearer token. It is drawn on every start and kept in memory: never in a file, a record, a log,.envor a route's answer, never equal to the bearer token, and removed from the environment as it is read (the shell passes it asCHIMERA_SCREEN_TOKEN). Any token read from disk βservice.token, aCHIMERA_SERVER_TOKENin.envβ still needs the per-question code for a yes; a refusal needs no code.chimera serviceholds no screen credential, so there every yes takes the code.chimera appin a browser puts it in the page it serves to a loopback request (<meta name="chimera-screen">), exactly where and when that page already carries the bearer token β the same exposure, by the owner's decision of 2026-10-16. - The background service.
chimera service startruns the same backend without a window. It publishes~/.chimera/service-<channel>.json(0600, written atomically;CHIMERA_SERVICE_DIRmoves it) with the owner's persistent token (<home>/service.token, or an owner-chosenCHIMERA_SERVER_TOKEN), and a client trusts that file only afterGET /api/infoanswers with the same pid, version, id and data folder. That token opens the whole API, so it stays apart from the bridge's: nothing on the MCP side reads the service files, and the bridge refuses them as files. The installed app's launcher record,~/.chimera/desktop-install.json, holds only four path variables βCHIMERA_HOME,CHIMERA_WORKSPACE,CHIMERA_SHELL_PREFS,CHIMERA_SHELL_STATEβ its writer refuses any other name, andstartvalidates it before running anything from it.
Full control through the bridge
The desktop bridge (chimera mcp desktop) has three switches, each meaningless without the one
before it: Allow Claude to operate this app (what the screens do, under the owner's posture),
Full control (answering approvals, editing the settings that are not the owner's), and, since the
owner's decision of 2026-10-18, Full control through the bridge on Settings > Security.
- Only the screen turns it on.
CHIMERA_BRIDGE_FULL_CONTROLis not in the Settings screen's allowlist, soPATCH /api/configrefuses it to every caller and the bridge'ssettings.editrefuses it by name. Its one route,POST /api/security/bridge-full-control, refuses any request the bridge forwarded, and turns the switch on only for a request that carries the screen credential (above) β the owner's bearer token alone isservice.token, readable on disk. The screen asks for confirmation first. Turning it off needs no screen: it only narrows. - What it adds. The owner's decisions (folder grants, saving an agent with its tool grants,
remembered approval rules, reject-all, remembering a project, answering a settings suggestion,
plugins and marketplaces, worktree cleanup), the routes that take the person's
confirm: true(restoring, deleting and exporting a design, the storage card's cleanups, restart and reset), the owner's settings β posture, guards, approvals, caps, models and scheduling, written instead of suggested β and opening a screen in the running app (ui.navigate, an address checked against the app's own screens, never a URL). - What it never adds. Credentials: no key, token, password or OAuth secret is read or written,
the Runner (a command outside governance can read the
.env) and adding an MCP server or signing in to one stay the owner's, and the key vault, the agent's read of Chimera's.envand the provider base URL are not writable. And who can reach the app: pairing, sharing, the LAN door, the server token, the messaging bots and their allowlists. Those refusals are inbridge_routes.FULL_CONTROL_NEVERandFULL_CONTROL_NEVER_SETTINGS, with the sentence a client receives. - The kernel still decides first. An approval answers a REVIEW; a kernel BLOCK never reaches an approver, so no yes β the screen's or the bridge's β runs it. The per-run body checks stay too: a run the bridge starts uses the configured models and posture; with full control those are changed in the settings, where the change is recorded.
- Visible and recorded. While it is on, every screen of the app shows a banner with a one-click
Turn off, and every call made on its authority writes an audit entry (
via: "bridge",scope: "full_control", the route id, the status and the ids it named β never a body or a value). The Security tab lists the newest of them, and the switch's own changes. - Honest limit. The switch lives in
.env, like the other two: a process that can write that file can turn it on without the screen. The agent's own tools refuse the file, and so does the bridge; anything else running as the owner does not.
What the owner decides, and what a repository cannot
- Deployment policy, deny-only.
<CHIMERA_HOME>/policy.json(or the fileCHIMERA_POLICY_FILEnames) holds orderedprovider.use/integration.use/tool.userules; the last match wins. Anallowthere never grants anything: it only undoes an earlier deny of the same file, so it cannot reopenCHIMERA_TOOL_DENYLIST, the reach floor or a kernel BLOCK. An invalid file is never applied in part:serve,chat,assist,tuiandacprefuse to start,doctorfails, and the desktop starts but refuses every tool until it is fixed. The agent cannot rewrite it β the write tools refuse the file wherever it lives, and the bridge cannot repoint it. - "Always in this project". On an approval card the owner can remember a yes for one project:
the tool, or for a shell command its first two words. It answers matching questions later,
including after the run read untrusted content β the owner's decision of 2026-10-15, knowing
a hostile page could steer a command inside that prefix. It only skips the question: a kernel
BLOCK and the reach floor still win, and a command that chains, pipes or redirects (
;,&&,|,>,$(β¦) never matches. Only the owner creates or revokes one; the bridge cannot, unless the owner turned full control through the bridge on (above). - Named references. Folders the owner lists in
<CHIMERA_HOME>/references.jsonare reachable as@alias/pathby the read tools only (read_file,list_dir,grep,glob); every writer refuses an alias. A filesystem root, the user's home or the data folder is refused as a reference. Only attended surfaces bind them βchat,assist,tui, an attendedsolve, the desktop's Code turn (never a guest's) β so bots, cron andscenarioshave none. - Agents in Markdown.
.chimera/agents/*.mdin a repository is someone else's text, so it can only narrow: its tools are intersected with the session's governed tools, its steps are capped by the session's, unknown keys are refused, itsmodelis ignored, and its body is fenced as untrusted data. Only the owner's files in<CHIMERA_HOME>/agentsmay pin a model from the catalogue. - Chats linked to conversations.
/linkin Discord or Telegram makes a chat continue a Code conversation, and only the owner listed in that bot's allowlist can link one β with no allowlist, nobody. A stranger's text in a linked room never reaches the conversation, and media is refused rather than passed on. The turn runs on the bot's floor, not the desktop's: only the MCP servers inCHIMERA_BOT_MCP_SERVERS, no OpenAPI connectors, no shell grant or host exec. - The owner's connectors stay off doors a stranger can open. An OpenAPI connector calls out with
the owner's key, so it answers whoever drives the run. It does not load on a messaging surface
while any configured bot has no allowlist, never on a GitHub issue job (anyone can write the
issue; only the repository is checked), and on
/v1/chat/completionsonly whileCHIMERA_SERVER_TOKENis set. One module,chimera/governance/surfaces.py, decides what kind of surface a name is, by its family (platform:discordis a bot); a test fails if a module writes its own list again.
Honest limits
This measures whether an already-injected agent's harmful action is stopped β not whether the model can be injected in the first place. Free-form reasoning over untrusted prose, and exfiltration through legitimately-needed tools, remain open problems (tracked as issue #5).
The full, always-current policy lives in SECURITY.md, including how to report a vulnerability.
Session permission bypass limits
The optional code-session permission bypass is authenticated with an HMAC key generated in process memory; that key is never written to disk or inherited by child processes. Persisted bypass_permissions flags and queued via_screen attestations without a valid HMAC are ignored. The permission choice consequently lasts only until the app/service process restarts, when bypass defaults off. A shell can still edit the JSON, but cannot forge the attestation. Owner decision (26/10): with the switch on, the agent releases everything it would otherwise ask about β git and pushes to GitHub, gh, MCP connector tools, deploys, uploads, HTTP writes and send tools, in a clean run and in a tainted one. Three things still ask the owner, and nothing else does: (a) the kernel's BLOCK rules and the destructive denylist (rm -rf /, mkfs, ...); (b) sending or reading a secret file (.env, keys, .npmrc, .netrc, .pypirc, .git-credentials, AWS credentials) as part of an outbound command, and PowerShell -EncodedCommand; (c) rewriting the default branch's history on a remote β a force push, a +refspec, or a delete of main/master (git push --delete, git push origin :main), including the same written inside bash -c or powershell -Command, and gh api deletes of the default ref. A force push to a feature branch is released. These checks are lexical heuristics, not a security boundary: glob-expanded names such as cat .e*, dynamically assembled strings such as python -c "open('.'+'env')", alternate encodings, wrappers, or commands outside the recognized syntax cannot be guaranteed to be detected. A malicious page can still bypass them. Use the isolated sandbox for untrusted work.