Skip to content

Security boundaries

Mjolnir gives coding agents the ability to execute commands. Its security model is therefore a set of explicit boundaries, not a promise that agent-generated commands are harmless.

Bare runtimes preserve approvals where the harness supports them; isolated runtimes run unrestricted. A disposable boundary limits damage to the runtime, but it does not prevent the agent or its model provider from reading data and credentials intentionally placed inside that boundary.

Your agent’s own traffic goes to its model provider under your account, as it would without Mjolnir. Apart from that, a default installation sends the following.

FeatureWhat is sentWhere it goesHow to turn it off
Jev turn classifierAfter a minute of silence in a turn, and after a reply ends: up to 1 KiB of your latest prompt, up to 2 KiB of the latest assistant text, a size-limited transcript summary, tool titles (up to 128 bytes each), the harness name, and counts of background and queued commands.The public Cloudflare proxy mj-jev-proxy.eng-admin-a63.workers.dev, which forwards it to TypeSafe (api.typesafe.ai). With TYPESAFE_API_KEY set, or a key in ~/.secrets/typesafe_api_key, it goes straight to api.typesafe.ai with your key.[jev] enabled = false, or Settings → Privacy → Jev (hosted service). A blank key does not disable it; it falls back to the proxy.
Automatic continuationWhen a reply ends and the agent may have stopped early: your messages since the last context reset and recent assistant replies. Tool history is not sent.The same proxy (/v2/continuation-verdict), or api.typesafe.ai with your key.[continuation] enabled = false or [jev] enabled = false in config.toml, or clear Enabled under Settings → Continuation.
Semantic help searchWhile you type in the help filter, 200 ms after the last key: the filter text and the text of every help row.The same proxy (/v1/help-search), or api.typesafe.ai with your key.[jev] enabled = false. It runs only while the help filter has text.
Update checkThe installed version’s channel is asked for the latest release. No session content is sent.GitHub Releases, the npm registry, or the Homebrew tap on GitHub.Set MJOLNIR_NO_UPDATE_CHECK.

One switch stops every Jev request: set enabled = false under [jev] in config.toml, or clear Settings → Privacy → Jev (hosted service). Then nothing in the first three rows leaves the machine from the dashboard or from sessions started afterwards. Workers read the switch when they start, so a session that was already running keeps sending turn text until you resume or restart it. What you give up: a turn ends only when the harness ends it, so a turn that goes quiet while the agent waits for you stays Working until the harness reports the end of the turn; automatic continuation does not run; and help search matches text only. See Configuration.

The proxy source is in services/jev-proxy/. It does not write request bodies to logs, and it limits requests per client IP address. Mjolnir keeps a local copy of each Jev request in jev-decisions/ under its data directory; see Session lifecycle.

Execution policy is selected by the runtime

Section titled “Execution policy is selected by the runtime”
RuntimePolicy
bare on the local machinePreserve the selected profile and harness’s configured approvals
bare on an ssh machine with permissions = "guardian"Preserve configured approvals
bare on an ssh machine with permissions = "yolo"Force unconstrained execution
podman, docker, apple-container, and bare on an aws-ec2 machineForce unconstrained execution

Mjolnir translates the unconstrained policy into the selected harness’s own controls:

HarnessUnconstrained enforcement
Codexagent-full-access
Claude CodebypassPermissions with its sandbox disabled
Kimi Codeauto
Grok Buildalways approve with its sandbox disabled
Muse Codethe :ask-me permission profile in its staged settings, promptUnmatched approvals with auto-review on, and --disable-sandbox (the reviewer can still deny)
OpenCode"permission": "allow" in the staged opencode.json
MiMo CodeMIMOCODE_DANGEROUSLY_SKIP_PERMISSIONS=1
DSHdsh-acp’s permission_mode allow, selected on every session open

Kimi’s mode is named auto, but in this context it approves every call. It is not a low-risk guardian policy.

Codex, Claude Code, Grok Build, and Muse Code can preserve guardian-style approvals on a bare runtime. Muse’s guardian is muse-acp’s auto-review: Mjolnir stages the :ask-me permission profile, selects promptUnmatched approvals, and turns auto-review on, so a read-only Muse reviewer answers each approval and a failed review denies the action. Muse keeps that review in containers too and disables only the sandbox there; with guardian approvals the sandbox stays on, even when a profile’s MUSE_SERVE_ARGS passes --disable-sandbox. Kimi Code cannot preserve approvals. OpenCode and MiMo Code ask before tools according to their permission rules, but run every command directly on the host: both projects state that their permission system is not a sandbox. DSH under configured approvals selects dsh-acp’s permission_mode ask, so every write, command, and MCP tool call asks first, but it has no sandbox either: commands run directly on the host, and only its file tools are confined to the workspace roots. The dashboard’s new-session and resume wizards warn when a harness without guardian support is paired with a raw target; mj new, the API, and the web viewer do not. A warning is not a sandbox either way—choose a container or instance instead.

Mjolnir does not expose arbitrary extra container-runtime arguments. Container names, ownership labels, capabilities, and generated mount modes come from Mjolnir and the selected runtime rather than from config.toml. A chosen image and runtime must therefore already provide any capability their workload or mount contract requires. See Container targets and the Configuration reference.

Decide what belongs inside the trust boundary

Section titled “Decide what belongs inside the trust boundary”

The controller host is trusted with the canonical configuration, session database, recovery archives, profile credentials, and cookie-signing key. A live target is trusted with:

  • the repositories and uncommitted work placed in its workspace;
  • the allowlisted profile files needed to run the selected harness;
  • live harness and, on non-local sessions, GitHub credentials;
  • any extra directories or images attached to the session; and
  • prompt text, tool output, and conversation state delivered to the harness.

Anyone who controls the target at a sufficiently privileged level can inspect those live copies. Isolation protects the rest of the controller host; it does not make copied secrets unreadable inside the target. Likewise, provider-side handling of prompts and repository content is governed by the selected harness and account, not by Mjolnir.

Mjolnir never copies a harness home wholesale. It builds a per-session profile home from a harness-specific allowlist:

HarnessCategories staged into a session
CodexAuthentication, config, instructions, rules, and skills
Claude CodeAuthentication and account config, settings, CLAUDE.md, skills, and plugins
Kimi CodeAuthentication, config, device ID, instructions, MCP config, skills, agents, and plugins
Grok BuildAuthentication, config, agent ID, instructions, skills, and plugins
Muse CodeAuthentication, settings, trust configuration, instructions, skills, and rules
OpenCodeAuthentication, configuration, instructions, and skills
MiMo CodeAuthentication and the complete config/ tree, including instructions, skills, agents, and plugins
DSHInstructions and skills (which DSH does not read); its API key reaches the session through the environment, not a file

Symbolic links inside an allowlisted profile entry are followed: the session receives the contents of the file or directory a link points to, even when the target is outside the harness home. Check where your profile’s links point before you use it with a target. A link whose target is missing is skipped. Files outside the allowlist—such as general shell state, unrelated cloud credentials, and arbitrary caches—do not enter the session merely because they live beneath your home directory.

The staged skills tree also carries the Mjolnir-authored mj skill. It does not carry the skills a harness writes for itself: the skills Claude Code syncs from your claude.ai account (skills/synced/), Claude Code’s skills/.trash/, and Codex’s built-in skills (skills/.system/). The harness manages those itself. The mj-memory MCP history tools can read the controller’s indexed session corpus, including conversations from other projects. Historical conversations are reference data, not instructions for the current session.

The staged profile is still active configuration. Instructions, plugins, skills, and MCP settings can execute code or direct an agent to external services. Audit them as part of the selected profile’s trust boundary. See Profiles and harnesses for profile setup.

The profile on the controller is the canonical copy. While sessions are live, Mjolnir reconciles rotating harness credentials between that profile and its session copies about once a minute. Skills are synchronized from the canonical profile into live sessions on the same cadence.

Credential bytes travel only in private controller-to-worker request and response frames. They are structurally excluded from the durable event journal and recovery archive. Non-secret fingerprints and freshness timestamps may appear in diagnostics.

If the controller’s gh CLI is authenticated, Mjolnir also pushes its active GitHub token into every live non-local session, including raw SSH targets. The token is not included in recovery archives. Its effective authority is still the authority granted to that GitHub account, so a remote target receiving it belongs inside the token’s trust boundary. Raw local sessions do not receive this synchronized GitHub token.

Rotating OAuth grants can race when several live copies refresh at once. Mjolnir refreshes Codex credentials ahead of expiry and distributes the newer copy. For Claude Code, use a long-lived setup token when running concurrent or unattended sessions:

Terminal window
mj login --profile <claude-profile-id> --setup-token

That token is stored in Mjolnir’s private controller data, outside the Claude profile home, and is passed to new and resumed Claude sessions as CLAUDE_CODE_OAUTH_TOKEN. It authorizes model requests; it does not enable Claude Remote Control or claude.ai connectors.

Isolated sessions use independent network clones

Section titled “Isolated sessions use independent network clones”

A bundle repository declared with local = "/absolute/path" supplies its configured default network fetch and push destinations. Mjolnir clones the fetch remote’s default branch into the target and starts on that branch. The target has no Git connection back to the controller checkout.

Local unpublished commits and staged, unstaged, or untracked files are not copied. Normal Git pushes go to the configured network push destination(s). Suspending saves a checkpoint without publishing a branch into the host repository. Only raw local sessions support repositories without network remotes. See Workspaces and bundles.

An additional directory selected in the new-session or resume flow makes that directory’s contents readable by the agent. Do not attach a parent directory when the agent only needs one child, and never use a read-only choice as a confidentiality control—it prevents writes, not reads.

Podman, Docker, and Podman-over-SSH normally present writable attachments through a copy-on-write overlay. Agent writes go to session-owned storage and do not modify the source directory. A source on a known-incompatible filesystem such as NFS, SMB, FUSE, FAT, or another overlay is downgraded to read-only and the launch reports it. Apple containers use read-only attachments. EC2 receives a copied resource directory rather than a host mount. Bare local and bare SSH targets do not accept additional directories.

Attachment storage is not the recovery boundary. Put durable results under the session’s project workspace or push them to a repository before stopping. See Container targets for runtime-specific behavior.

The web viewer can also attach images to a prompt when the harness advertises image support. Unsent images remain only in that browser’s memory; stored drafts retain text, not images. Once sent, the image is delivered to the controller and the selected harness as prompt content. Treat it exactly like any other data disclosed to the agent and model provider.

The personal web viewer is enabled by default. mj daemon status prints its URL and a six-digit login code. The code is exchanged for a signed session cookie; protected snapshot, transcript, draft, and action APIs all require a valid cookie. Cookies are HTTP-only and same-site, are marked secure under HTTPS, and expire after 30 days without authenticated requests by default. Authenticated requests renew new phone-login cookies while preserving the viewer identity. Desktop bootstrap cookies and cookies issued by older builds retain their original expiry; sign in again to opt an older phone cookie into renewal.

The six-digit code is intentionally convenient rather than high entropy. Five wrong codes lock the login endpoint. Repeated lockouts back off from 30 seconds to a maximum of one hour, and a correct code clears the failure history. Do not publish the code or an authenticated browser session.

The QR login URL contains a strong secret derived from the cookie-signing key in Mjolnir’s private data directory. It stays valid across daemon restarts. Save the original QR login URL as a private bookmark if you want to sign in again after the browser clears its cookies; the redirected / URL does not contain that credential. The six-digit code still changes at restart.

Anyone who captures the QR or its login URL can sign in until the key is rotated. Signing out revokes that viewer identity and clears its cookie, but does not revoke the QR URL or sign other viewers out. Revocations persist in phone-cookie-revocations.json beside the signing key, preventing delayed responses from restoring access even after a daemon restart. If saving logout fails, the server reports an error; retry logout once storage is writable. Keep this file with the signing key; an unreadable or corrupt file prevents viewer startup rather than accepting revoked cookies.

To revoke all cookies and saved login URLs, stop the daemon, remove phone-cookie-key from its instance data directory, then start it again. Keep the same durable data directory across restarts to preserve access.

Cookie rejection diagnostics at debug level distinguish an absent, malformed, expired, revoked, or incorrectly signed cookie without recording credentials. An absent cookie may mean the browser evicted it; a bad signature may indicate key rotation. Mjolnir has one personal viewer trust domain; it does not provide per-user roles or session-level authorization.

Without trusted TLS, the server remains on loopback. With automatic Tailscale detection, Mjolnir exposes the viewer on the tailnet only after it obtains a trusted ts.net certificate. Explicit non-loopback service requires both a TLS certificate and key. Mjolnir refuses a non-loopback plaintext listener.

See Web viewer and desktop app for setup and Troubleshooting for connection and login failures.