Profiles and harnesses
A profile connects Mjolnir to one installed coding-agent harness and one account. Create multiple profiles for multiple accounts, even when they use the same harness. Every new or resumed session chooses a profile; the profile ID is also the row shown in the dashboard’s Profiles pane.
Profiles point at controller-side harness homes. They do not pin a model or
reasoning effort. Choose those per session with /model and /effort, so the
available choices can come from the provider’s current catalog.
Configure Mjolnir sub-agents in Settings → Sub-agents. Set a model and, when offered, effort for Straightforward tasks. Subtle is optional; if you set it, choose a smarter model or higher effort than Straightforward. Subtle cannot be set without Straightforward. These choices apply globally. With both set, parents choose a task type each time they delegate work. With only Straightforward, parents delegate straightforward work and do work that needs judgment themselves.
Oracle is also optional, and independent of the other two. Set a strong
model for read-only questions that the primary agent isn’t sure about; sessions
then get an ask_oracle tool whatever their delegation choice. Create and
Move… show an Oracle row when a model is set.
Create and Move… offer Native or Mjolnir delegation. Mjolnir
requires a Claude, Codex, DSH, or OpenCode profile and a configured Straightforward task.
DSH has no sub-agents of its own, so for a DSH profile they offer Mjolnir or
None instead. A DSH session starts on Mjolnir when the last choice was
Mjolnir and Straightforward is configured, and on None otherwise. Its choice
becomes the remembered one on interactive creation only when it is Mjolnir, so choosing None for DSH
does not change what the next Claude or Codex session starts with.
OpenCode has no steering, so a child’s report reaches an OpenCode parent when
its current turn ends, not between its tool calls. An OpenCode older than 2.2.6
(for example in a container built from an older image) loads its MCP tools
shortly after it connects, so the worker holds its first prompt for 250 ms.
Under configured approvals OpenCode asks the person before each mj-agents
call; choose “always allow” to stop it asking for the rest of the session.
Existing sessions keep their recorded policy; leaving Move’s choice unchanged
retains it.
Usage by session
Section titled “Usage by session”Open the dashboard’s Profiles title menu and choose Usage by session, or find it in the command palette. Each parent session shows its recorded usage by model and estimated API cost, including its Mjolnir sub-agents. Rows are sorted by total cost, highest first. Accounting retained for destroyed sub-agents still contributes to their parent.
Claude’s native sub-agent usage also contributes when the bridge reports a per-model breakdown. These counts include internal calls such as compaction. Earlier recorded turns that discarded this breakdown cannot be split by model retroactively.
The profile dropdown starts on All. Choosing a profile counts only the
turns that ran on that profile, wherever they are in a parent’s sub-agent
tree, and still shows them under the parent. For example, a Claude session
whose sub-agents ran on codex2 appears under codex2 with the cost of
those sub-agents’ turns. Parents on the chosen profile appear even when they
have no usage in the period. Earlier versions did not record each turn’s
profile, so their turns count under the session’s profile at the time of the
upgrade, and usage from sub-agents destroyed before the upgrade appears only
under All. Hourly is the default period;
Daily covers the trailing 24 hours; Weekly and Monthly cover the
trailing seven and thirty days. Check
Include suspended sessions to include inactive parents. The report refreshes
every thirty seconds, and Refresh reloads it immediately.
Costs assume standard base API token rates, including cache reads and default cache writes, even for subscription profiles. They exclude tools and speed, region, or long-context premiums. Only completed turns with complete usage reports enter the estimates. A + marks partial accounting, and unpriced marks models or counters that cannot be priced from the recorded information.
Quota history
Section titled “Quota history”While the daemon is running, it records each quota refresh result in the
quota_history table of its mj.sqlite3 database. Profiles normally refresh
every ten minutes; provider rate limits can extend the interval. Manual and
other requested probes also contribute. History starts with the first refresh
after upgrading; earlier cached reports are not backfilled. Startup reuses of
the cache do not add samples. History has no automatic expiry and survives
session or profile removal.
Each row has an id, a profile_id, a harness, a
sampled_at_epoch_seconds timestamp (UTC Unix seconds, at refresh start), a
hashed configuration identity, and a JSON body containing the complete
quota report. The identity distinguishes changes to a profile’s quota source;
it does not identify the subscription account. Profiles sharing an account
may report the same allowance. Multiple probes can share a timestamp; id
distinguishes them.
The report retains every window, including model-specific allowances, remaining percentages, reported usage and limits, reset times, and banked resets when available. These are the normalized quota readings, before filling missing reset data from the cache or substituting previous windows after a failed probe. Unchanged readings and failures are retained too. Missing values remain unknown; API-billed and non-reporting profiles have no quota windows. A quota reader may reuse its own recent provider response; sampling is not a guarantee of a new provider measurement.
For example, open the database with sqlite3 -readonly /path/to/mj.sqlite3
and list windows for one profile:
SELECT h.id, datetime(h.sampled_at_epoch_seconds, 'unixepoch') AS sampled_utc, h.identity, w.value ->> '$.label' AS window, w.value ->> '$.model_scope' AS model_scope, w.value ->> '$.remaining_percent' AS remaining_percent, w.value ->> '$.used' AS used, w.value ->> '$.limit' AS allowance, w.value ->> '$.duration_seconds' AS duration_seconds, w.value ->> '$.resets_at_epoch_seconds' AS resets_at_epoch_seconds, h.body ->> '$.banked_resets' AS banked_resets, h.body ->> '$.error' AS error, h.body ->> '$.extra' AS note, h.body ->> '$.rate_limited_until_epoch_seconds' AS probe_held_untilFROM quota_history hLEFT JOIN json_each(h.body, '$.windows') w ON trueWHERE h.profile_id = 'codex'ORDER BY h.sampled_at_epoch_seconds, h.id, w.key;The left join includes failed probes and reports without windows. For cost
analysis, compare decreases within the same configuration identity, window,
model scope, and reset period with the Usage by session API cost estimates
for that profile and period. Short and weekly allowances overlap; do not add
them together. Resets, banked-reset use, rounded percentages, gaps between
samples, incomplete token reports, and account usage outside Mjolnir limit
precision. A reset between probes can hide consumption. History does not record
the subscription price or plan changes, so supply those separately. Given a
comparable period, the effective discount is
1 - subscription cost / estimated API cost when the estimated API cost is
positive. Estimating a fully used plan from a partially used quota also assumes
the model mix and quota charging stay comparable.
Supported harnesses
Section titled “Supported harnesses”| Harness | kind | Home variable | Conventional home | Authentication marker | Guardian approvals on a raw target |
|---|---|---|---|---|---|
| Codex | codex | CODEX_HOME | ~/.codex | auth.json, or config.toml for an API-key provider | yes |
| Claude Code | claude | CLAUDE_CONFIG_DIR | ~/.claude | .credentials.json | yes |
| Kimi Code | kimi | KIMI_CODE_HOME | ~/.kimi-code | credentials/kimi-code.json | no |
| Grok Build | grok | GROK_HOME | ~/.grok | auth.json | yes |
| Muse Code | muse | XDG_CONFIG_HOME (parent of home) | ~/.config/muse | auth.json | no |
| OpenCode | opencode | XDG_CONFIG_HOME (parent of home) | ~/.config/opencode | a login in opencode.db, or a provider key in the environment | no |
| DSH | dsh | DSH_ACP_DATA_DIR | ~/.dsh | DEEPSEEK_API_KEY saved in DSH, or in the environment | no |
There are seven harness kinds. A profile whose kind is mimo, for the
removed MiMo Code harness, is ignored with a warning when the configuration
loads and is removed from the file on the next save. Codex can use its built-in Amazon Bedrock
providers; see Codex on Amazon Bedrock. It can also
authenticate with an API key against a custom provider; see
Codex with a custom provider.
A session never runs from the profile home itself. Every session, on every target including a bare runtime on this machine, runs from a staged copy of the home that belongs to the session, and the harness’s home variable points at that copy. See What a session’s staged home holds.
mj setup checks the home variable first and otherwise looks in the
conventional location. It also treats an API key in the environment Mjolnir
started with as an installation signal for a harness that authenticates with
no login, such as DEEPSEEK_API_KEY for DSH. A detected home becomes the
explicit home path in config.toml; subsequent sessions use that configured
path.
Kimi Code does not expose a guardian approval mode. OpenCode asks before tools according to its permission rules, and DSH asks before every write, command, and MCP tool call other than Mjolnir’s own, but both run commands on the host without a sandbox, so they have no guardian either. The dashboard’s new-session and resume wizards warn before using any of them on a raw target. Muse’s guardian is muse-acp’s auto-review; see Harness limitations. Container runtimes and EC2 machines instead run every harness unconstrained inside the isolation boundary. See Targets and Security boundaries.
Codex on Amazon Bedrock
Section titled “Codex on Amazon Bedrock”Codex 0.159.1 includes the built-in providers amazon-bedrock and
amazon-bedrock-runtime. Select one directly in the profile home’s
config.toml; it does not need a [model_providers.<id>] table, base_url,
wire_api, or an API key. Codex signs requests with AWS SigV4 using its AWS
credential chain, which can use environment credentials, an AWS profile, or an
EC2 instance role.
For GPT-6 Luna through a Bedrock Runtime inference profile, put this in
/home/ubuntu/.codex-bedrock/config.toml:
model = "global.openai.gpt-6-luna"model_provider = "amazon-bedrock-runtime"model_reasoning_effort = "high"
[model_providers.amazon-bedrock-runtime.aws]region = "us-east-1"aws.region is an optional Codex override. Codex can also read
AWS_REGION or AWS_DEFAULT_REGION. Configure the Mjolnir profile like this:
[profiles.bedrock-luna]kind = "codex"home = "/home/ubuntu/.codex-bedrock"
[profiles.bedrock-luna.environment]AWS_REGION = "us-east-1"AWS_DEFAULT_REGION = "us-east-1"Mjolnir passes profile environment entries to the harness on local, container,
and remote targets. Use { from_env = "AWS_PROFILE" } for an AWS profile name
from the environment Mjolnir started with, or { from_secret = "NAME" } for
AWS access keys or session tokens. The AWS config and credentials files named
by AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE must exist on the target
where Codex runs. An EC2 instance role needs no key variables or credential
files. For Podman sessions on EC2, set the host’s instance-metadata response
hop limit to 2 so the container can reach IMDS, and leave
AWS_EC2_METADATA_DISABLED unset or false.
The AWS account must have access enabled for the requested Bedrock model and
inference profile. mj models --profile bedrock-luna asks Codex for its
built-in Bedrock catalog; Mjolnir does not try to fetch /models with an API
key. If Codex reports no models, the command says so and the model configured
in config.toml remains the profile’s default.
Limiting the AWS credentials sessions receive
Section titled “Limiting the AWS credentials sessions receive”By default a Bedrock profile’s container and SSH sessions receive a copy of the controller host’s AWS credentials, so code in a session can do whatever that identity can. To give each session credentials limited to one IAM role instead, add a table to the profile. It works for Claude Code and Codex Bedrock profiles alike:
[profiles.bedrock-luna.aws]role_arn = "arn:aws:iam::123456789012:role/mj-bedrock-session"# session_policy = '{"Version":"2012-10-17","Statement":[...]}'# session_policy_arns = ["arn:aws:iam::aws:policy/AmazonBedrockReadOnly"]# duration_seconds = 3600# source_identity = trueMjolnir assumes the role (AWS sts assume-role) with the host’s AWS login for
each session, names the role session mj-<session id> so CloudTrail shows
which session made each call, and installs the temporary credentials in the
session. It renews them when half of their lifetime has passed. The host’s own
credentials stay on the controller.
session_policy (inline JSON) and session_policy_arns (managed policies)
narrow the role further for the session; they can only remove permissions,
never add them. With neither set, Mjolnir uses a built-in policy that allows
invoking and listing Bedrock models and nothing else. duration_seconds
defaults to AWS’s own default of 3600 and may be 900 up to the role’s maximum
session duration; AWS rejects other values. source_identity = true also sets
AWS SourceIdentity, which the role’s trust policy must allow with
sts:SetSourceIdentity.
The controller’s AWS login (the profile’s AWS_PROFILE, or the default) needs
sts:AssumeRole on the role, and the role’s trust policy must name that login.
mj doctor assumes the role once to check this. If AWS refuses, a new session
fails to start with AWS’s error and what to change. A running session keeps the
scoped credentials Mjolnir issued until they expire, and has any other AWS
credentials it holds (the host’s own, from before you added the role) withdrawn;
the session’s other synchronization continues and the failure is reported. you can also remove the table to return to
copying the host’s credentials. A table that cannot apply to a session
has no effect there: sessions start without it, with a notice that names the
reason, and mj doctor warns, per target. The reasons are a profile that does
not authenticate with the AWS credential chain, a raw local target (it reads the
host’s own AWS files), an EC2 machine (it uses its instance role), and a target
or profile whose environment sets its own AWS profile or credentials. This applies only to container and SSH
sessions that receive synced credentials: raw local sessions read the host’s
AWS files directly, EC2 sessions use their instance role, and a Bedrock API key
(AWS_BEARER_TOKEN_BEDROCK) is not covered. The table is edited in
config.toml; Settings preserves it but does not yet offer a form for it.
Codex with a custom provider
Section titled “Codex with a custom provider”A Codex profile does not have to talk to OpenAI. Codex’s own config.toml
decides which service it uses, and Mjolnir copies that file into the staged
profile home unchanged. Point it at another service that speaks the Responses
API and the profile runs through the same Codex bridge as any other, with no
mj login and no OAuth.
Write the provider into the profile home’s config.toml. This example uses
Z.ai’s GLM Coding Plan:
model = "glm-5.3"model_provider = "zai"model_reasoning_effort = "high"
[model_providers.zai]name = "Z.ai coding plan"base_url = "https://api.z.ai/api/v1"env_key = "ZAI_API_KEY"wire_api = "responses"wire_api must be responses; Codex no longer supports the chat-completions
form. env_key names the environment variable that carries the API key. As
with Codex on its own, exporting that variable is enough: when the profile does
not set it, Mjolnir reads it from the environment it started with and passes it
to the session. The profile needs nothing beyond its home:
[profiles.glm]kind = "codex"home = "/home/me/.codex-glm"The daemon only sees variables from the environment it was started in, so
export the key before running mj, and run mj daemon restart after exporting
it in a new shell. To keep the key in a file instead, put it in secrets.toml
beside config.toml and name it in the profile; an entry the profile sets
always wins over the environment:
[profiles.glm.environment]ZAI_API_KEY = { from_secret = "ZAI_API_KEY" }A profile whose key is set in neither place cannot start: its sessions are
refused with an error naming the profile, the variable and how to supply it,
and mj doctor reports the same. The rest of the configuration, the daemon and
every other profile keep working.
A provider may instead inline its key as experimental_bearer_token. Mjolnir
reads that key the same way it reads env_key: the profile is authenticated,
the provider’s catalog is fetched with the inline key and staged, the Guardian
reviewer is stamped, and the provider can serve quota and utility work. Prefer
env_key, because the inline form writes the key into the profile’s
config.toml, which is copied into every staged home and travels with the
configuration.
What changes for such a profile:
- No login.
mj loginreports that the profile authenticates with its API key and exits non-zero. The profile counts as set up as soon as its Codexconfig.tomlexists, somj doctorreports it authenticated with no remediation line. There is no credential file to expire, refresh, or sync into a running session. - Models come from the provider. Mjolnir fetches the provider’s model catalog
from
{base_url}/modelsbefore each launch and stages it asmodels.jsonbeside the stagedconfig.toml. Without it Codex would offer OpenAI’s built-in model names and send them to your provider. If the provider is unreachable, the last catalog Mjolnir fetched for the profile is staged instead. Ifconfig.tomlsetsmodel_catalog_json, Mjolnir ignores the file it names and points the staged configuration at its own catalog; your file is never modified. Usemodels.jsonto shape the catalog. - Guardian reviews run on the newest flash model the catalog lists. Mjolnir
stamps that choice on every catalog entry, so a heavyweight session model is
not also its own reviewer. When the catalog lists no flash model, Codex reviews
with the session model. Change the choice with
guardian_review_model. - The catalog stays in the staged home. Like every session, the session runs from a staged copy of the profile home, so the generated catalog never lands in your own Codex directory.
- Quota is reported for Z.ai (
api.z.ai) and Zhipu (open.bigmodel.cn) hosts, which publish the Coding Plan windows. A provider on any other host, such as DeepSeek, is usage-priced, so its profile showsAPIin the quota column and counts as fully available when ranking sub-agent profiles. - Utility inference depends on the provider. Z.ai profiles cannot supply Mjolnir’s own transcript compaction. DeepSeek profiles can, using their chat-completions endpoint; see Durability and recovery.
Providers that serve a plain model list
Section titled “Providers that serve a plain model list”Some providers answer GET {base_url}/models with Codex’s own catalog format,
{"models": [...]}, which carries a full description of each model. Z.ai is one
of them. Most OpenAI-compatible providers instead answer with OpenAI’s plain
list, {"object": "list", "data": [{"id": "..."}]}, which carries only model
ids. DeepSeek is one of those:
model = "deepseek-flash"model_provider = "deepseek"model_reasoning_effort = "high"
[model_providers.deepseek]name = "DeepSeek"base_url = "https://api.deepseek.com/v1"env_key = "DEEPSEEK_API_KEY"wire_api = "responses"Mjolnir accepts both shapes. For a plain list it builds a catalog entry per id
with conservative defaults: a 128,000-token context window, the plain shell tool,
and no reasoning levels, so a session on such a model offers no effort choice
rather than offering one the provider would reject. DeepSeek V4 models
(deepseek-v4-*) instead get their 1,000,000-token window and compact
automatically at 800,000 tokens, the same point DeepSeek’s own harness uses.
Quota reporting is separate from the catalog and covers Z.ai (api.z.ai) and
Zhipu (open.bigmodel.cn) hosts only. A profile on any other provider, such as
DeepSeek, shows API in the quota column, because it is usage-priced rather
than a subscription window. Sessions on it run normally.
A DeepSeek profile can also supply utility inference for Mjolnir’s own work,
such as transcript compaction. It has the lowest precedence for that work and
uses the newest deepseek-*flash* model in the merged catalog. A Z.ai (GLM)
profile does not supply utility inference. See
Durability and recovery.
Refine the catalog with your own models.json
Section titled “Refine the catalog with your own models.json”Put a models.json in the profile home to correct or extend what the provider
advertises. It uses Codex’s catalog format. Each entry is matched to the fetched
catalog by slug; the fields you write replace those on the fetched entry and
every other field survives. A slug the provider did not list is added to the
catalog. For example, to give DeepSeek’s reasoning model its two effort levels:
{ "models": [ { "slug": "deepseek-v4-pro", "supported_reasoning_levels": ["low", "high"] } ]}Mjolnir merges this file into the fetched catalog for every launch and writes
the merged result as the staged models.json. Your own file is never edited.
A model_catalog_json key in the Codex config.toml is ignored: the staged
configuration is pointed at Mjolnir’s catalog instead, so the key never reaches
a target as a path into your own home.
Choose the guardian review model
Section titled “Choose the guardian review model”Set guardian_review_model on the profile to decide which model reviews an
escalated action:
[profiles.glm]kind = "codex"home = "/home/me/.codex-glm"guardian_review_model = "glm-5.3"The accepted values are:
newest-flash, the default when the setting is absent: the newest flash model in the merged catalog reviews. Versions compare numerically, soglm-5.10-flashis newer thanglm-5.3-flash.session: nothing is stamped, so Codex reviews with whichever model the session is running. This costs more and is the setting to reach for when a provider’s small model reviews badly, for instance by denying benign actions.- Any model slug from the merged catalog, such as
glm-5.3above. A slug the catalog does not list fails the launch with an error naming the slug and the slugs the catalog does list, rather than quietly reviewing with something else.
The setting applies only to a Codex profile with a custom provider, because Mjolnir generates a catalog only for those. Setting it on any other profile is a configuration error.
Limit the models a profile offers
Section titled “Limit the models a profile offers”Some harnesses advertise very large catalogues. OpenCode with an OpenRouter key
lists several hundred models, and they all appear in session model pickers,
sub-agent choices, review slots and Autoconfigure. OpenCode ignores its own
provider whitelist and blacklist, so Mjolnir has its own list. Set models
on the profile to the exact model values you want offered:
[profiles.ds8bit]kind = "opencode"home = "/home/me/.opencode-ds8bit"models = ["openrouter/deepseek/deepseek-v4.1-flash-1017"]mj models --profile ds8bit --unrestricted prints every value the profile
advertises, ignoring models. Without --unrestricted, mj models prints what
Mjolnir offers. A profile without models, or with an empty list, offers
everything it advertises. You can also edit the list in Settings, under the
profile’s Offered models.
The profile’s current model stays offered even when models omits it, so a
session never runs a model its picker hides. Edits to models take effect
without rediscovering the profile’s catalogue. A Settings draft that changes
only models shows the saved list in its pickers until you save.
mj doctor warns when an enabled profile advertises more than 50 models and has
no models list, and gives the line to add. It also warns about entries the
profile does not advertise and about a current model outside the list. The check
reads the catalogue from a running daemon; without one, or while discovery is
still running, it reports “Not checked” and does not fail.
Codex profile files
Section titled “Codex profile files”Codex reads profile files beside its config.toml: codex -p ds layers
$CODEX_HOME/ds.config.toml over config.toml. Mjolnir offers each such file
as a profile of its own, so you choose it when you create a session, in the
Profile step or with mj new --profile codex-ds.
Launching mj detects the files. For every <name>.config.toml in a detected
Codex home it adds a profile named codex-<name> that uses the same home with
codex_profile set. You can also write one yourself:
[profiles.codex-ds]kind = "codex"home = "/home/me/.codex"codex_profile = "ds"The bridge Mjolnir runs Codex through starts codex app-server, which refuses
--profile. So Mjolnir layers the file itself the way Codex does. Tables merge
key by key at every depth, and any other value in the profile file replaces
the one in config.toml. The merged result is written into each session’s
staged config.toml. Your own files are never edited.
Everything Mjolnir reads from Codex’s configuration uses the merged result: the
model provider, how the profile authenticates, the provider’s model catalog,
quota, and the default model. A ds.config.toml that switches
model_provider to a custom provider gets
that provider’s catalog and key handling, while the plain codex profile keeps
its own.
The name must be ASCII letters, digits, - or _, as codex -p requires.
When the named file is missing or does not parse, the profile is unusable and
mj doctor and every launch say so. Codex itself would quietly run without the
file instead.
Configure a profile
Section titled “Configure a profile”The minimal shape is:
[profiles.codex-work]kind = "codex"home = "/home/me/.codex-work"Profiles are enabled by default. Set enabled = false to keep a profile’s
configuration without allowing Mjolnir to select or probe it:
[profiles.codex-standby]enabled = falsekind = "codex"home = "/home/me/.codex-standby"A disabled profile is omitted from new, resume, move, review, login, import, Quota, and utility-model choices. Sessions already running under that profile continue to operate. Re-enable it before starting new work with it.
The new-session and resume pickers also leave out a profile that is not there:
one a harness cannot start from (for example, its API key is not set), or one
whose agent is gone, meaning its home directory no longer exists and its
command is not on PATH. The picker names it under the list with the reason
and the fix. Mjolnir checks when the dashboard starts and each time a picker
opens, and the profile returns as soon as a check finds it again. Each check
also adds a profile for an agent installed since the configuration was
written.
Optional environment and compaction controls are useful for nonstandard target installs:
[profiles.codex-work]kind = "codex"home = "/home/me/.codex-work"context_window_bytes = 131072
[profiles.codex-work.environment]PATH = "/opt/node/bin:/usr/local/bin:/usr/bin:/bin"PROVIDER_SETTING = "value"home must be non-empty; use an absolute controller-side path. Environment
keys cannot be blank or contain =, and the harness’s own home variable cannot
be overridden there. context_window_bytes, when present, must be at least
32768. It is a conservative byte budget used when Mjolnir has to compact a
transcript across harnesses; it is not a model-token claim.
For the complete field and validation table, see the Configuration reference.
A Codex profile that signs in with ChatGPT never gets an API key. Mjolnir
removes OPENAI_API_KEY, CODEX_API_KEY, CODEX_ACCESS_TOKEN and
OPENAI_BASE_URL from its harness environment on every target, wherever they
were set: in the profile or target settings, in the target’s shell profile, or
in a container image. Otherwise Codex could use the key in place of the login.
The worker log says when it removed one. A profile signs in with ChatGPT unless
its config.toml names a custom model provider or its auth.json records
"auth_mode": "apikey". Those profiles keep the variables.
PATH discovery
Section titled “PATH discovery”On every bare runtime, whether on this machine, an SSH machine, or an EC2
machine, each worker performs one bounded probe of the account’s shell and
starts it the way a terminal does: as an interactive login shell (zsh -l -i
for zsh, bash -l -i for bash, and login argv[0] with -i for tcsh or csh).
The probe begins from a cleared environment, so
nothing the process that launched Mjolnir exported leaks into the session, but
it reads the same startup files a terminal reads. Tools that a version manager
adds to PATH — nvm, pyenv, asdf, mise, volta, sdkman, or Homebrew on Linux —
are therefore visible to sessions without extra configuration.
Shell aliases and shell functions are not environment variables, so they never
reach a session. An explicit [profiles.<id>.environment] PATH wins over the
probe. Agent-requested ! shell commands are different: they intentionally run
through bash -lc in the session user’s login environment.
Profile configuration changes take effect after the worker restarts or the session resumes. Credential and skill reconciliation has its own live sync path described below.
Log in
Section titled “Log in”The guided Set up managed flow creates a profile home under
<config dir>/profiles/<profile-id>/<harness>, where <config dir> is
~/.config/mjolnir on Linux or the path in MJ_CONFIG_DIR. These homes are
owned by Mjolnir, so its pinned harness can safely create or migrate the login
without touching an existing agent installation. The same flow is available
from first-run onboarding when no installed agent was found.
For Muse, configure kind = "muse" and a home ending in muse, for example
home = "/home/me/.config/muse" or /home/me/accounts/work/muse. Discovery
uses $XDG_CONFIG_HOME/muse when set. Mjolnir gives each session a private
configuration copy and stores native history under that copy’s .data/muse/
tree, including on a bare runtime on this machine. Do not override XDG_CONFIG_HOME or
XDG_DATA_HOME in the profile environment.
For OpenCode, configure kind = "open-code" and a home ending in opencode,
for example home = "/home/me/.config/opencode" or
/home/me/accounts/work/opencode. Discovery uses $XDG_CONFIG_HOME/opencode
when set. OpenCode 2 keeps its logins in its database, opencode.db, in its
data directory: $XDG_DATA_HOME/opencode (or ~/.local/share/opencode) for
your own ~/.config/opencode, and <home>/.data/opencode for any other home.
That database also holds every OpenCode session, so Mjolnir reads only the
logins from it, read-only. An OpenCode 1 login in auth.json, in the data
directory or under <home>/.data/opencode where Mjolnir kept it for OpenCode
1, still reaches sessions until OpenCode 2 takes that directory over, the way
OpenCode 2’s own upgrade carries it. Mjolnir gives each session a private configuration
copy with those logins, which the session imports when it starts, and keeps the
session’s own database under that copy’s .data/opencode/ tree, including on
a bare runtime on this machine. A login you add, change or remove reaches
running sessions within about a minute: each one reloads its OpenCode session
once it is idle, never during a turn, and continues the same conversation.
Logins come only from the profile; one made inside a session lasts until that
session next reloads. Checkpoints keep the session database without its logins. Do
not override XDG_CONFIG_HOME or XDG_DATA_HOME in the profile environment.
OpenCode also signs in to a provider from a key in its environment, such as
DEEPSEEK_API_KEY or OPENROUTER_API_KEY, with no login at all. As with
OpenCode on its own, exporting the key is enough: when the profile does not set
it, Mjolnir reads every provider key the pinned OpenCode reads from the
environment it started with and passes it to the session, along with the
variables a provider in your opencode.json or opencode.jsonc lists under
env and any variable that file substitutes as {env:NAME}. A stored login
wins over a key for the same provider, as it does in OpenCode. Export the key
before running mj, and run mj daemon restart after exporting a new one;
mj doctor names the keys a profile is using.
Mjolnir copies the profile’s opencode.json to each session unchanged and adds
its own permission rules on top, so any OpenCode setting in it applies. For example, to set a
default model and send every OpenRouter request for it to one provider, with
no fallback to others:
{ "model": "openrouter/deepseek/deepseek-v4.1-flash-1017", "provider": { "openrouter": { "models": { "deepseek/deepseek-v4.1-flash-1017": { "options": { "provider": { "only": ["morph"], "allow_fallbacks": false } } } } } }}The provider object under options is OpenRouter’s provider routing
preference, sent with each request. Provider names are the endpoint tags that
https://openrouter.ai/api/v1/models/<model>/endpoints lists, such as
morph for morph/fp8.
For DSH, configure kind = "dsh", for example with
home = "/home/me/.dsh". Detection accepts DeepSeek’s own dsh CLI, its
~/.dsh home, or DEEPSEEK_API_KEY in the environment Mjolnir started with,
so a key alone is enough to be offered a profile. DSH runs BrokkAi’s
dsh-acp 0.4.0, a DeepSeek coding agent
that speaks ACP itself, so there is no separate bridge. It is not the DeepSeek
harness earlier releases offered as kind = "deepseek"; that kind still fails
to load. The home is a DSH_ACP_DATA_DIR root. It holds an optional
AGENTS.md and dsh-acp’s native session records, sessions/<id>.json; there
is no configuration file, no login file, and no skills support. dsh-acp reads
AGENTS.md itself from the data directory, the session working directory, and
each additional workspace root. An existing AGENTS.md must be a regular
UTF-8 file of at most 1 MiB. Mjolnir checks the staged data-directory file,
including the notes it adds to describe a container or EC2 session’s
environment, and refuses to start a session whose file breaks any of those
rules. The refusal says which rule and by how much, and mj doctor reports the
profile’s own file the same way. Nothing creates the home automatically, and
it is optional: a session stages its own home, which simply starts without an
AGENTS.md. Create ~/.dsh when you want to keep instructions there or move
the profile; mj doctor reports it missing until you do. Mjolnir stages its
instructions as the data directory’s AGENTS.md and clears
DSH_ACP_SYSTEM_PROMPT so a target login shell cannot add a duplicate prompt.
The staged file is the profile’s own AGENTS.md plus any guidance Mjolnir adds
for the session: a disposable-environment note on container and EC2 targets,
container storage notes, and sub-agent instructions when sub-agent delegation
applies. On a bare local or SSH target with none of these, it is the profile’s
file unchanged.
DSH authenticates with a DeepSeek API key, DEEPSEEK_API_KEY. Mjolnir uses
the first of these that holds one:
- The key saved in DSH itself, under
refsin.credentials.yamlin the profile home (DSH’s own home,~/.dshby default), as DSH’s Models settings write it. DSH refuses that file when any other user can read it, so Mjolnir skips its key then, moves on to the next source, and names the file (chmod 600it) when no source has a key. DEEPSEEK_API_KEYin the environment Mjolnir started with.DEEPSEEK_API_KEYin.envin the profile home.- The profile’s
environment, for example fromsecrets.toml:
[profiles.dsh]kind = "dsh"home = "/home/me/.dsh"
[profiles.dsh.environment]DEEPSEEK_API_KEY = { from_secret = "DEEPSEEK_API_KEY" }When detection adds a DSH profile and secrets.toml beside config.toml
defines DEEPSEEK_API_KEY, it writes that environment entry for you.
The first three are the places DSH reads, so a session uses the key dsh
itself uses. A DeepSeek account sign-in is not an API key: dsh-acp cannot send
it, so a DSH that is signed in but holds no key is refused with that reason.
There is no interactive login, and mj login refuses the profile. A profile
without the key cannot start; the refused session and mj doctor say how to
supply it. Other dsh-acp settings can go in environment:
DEEPSEEK_BASE_URL (default https://api.deepseek.com), DEEPSEEK_MODEL
(default deepseek-flash), and DSH_ACP_MODELS (default
deepseek-flash,deepseek-v4-pro). Mjolnir owns DSH_ACP_DATA_DIR (set home
instead), DSH_ACP_SYSTEM_PROMPT (put instructions in AGENTS.md in the
profile home instead), and DSH_ACP_PERMISSION_MODE (the target’s permission
mode decides it). A profile that sets any of them in environment fails to
load.
Run:
mj login --profile codex-workYou can also sign in without leaving the dashboard: press prefix+s to open
Settings, choose Agent Profiles, select a profile, and choose Sign in. Mjolnir
hands the terminal to the same login flow and resumes the dashboard when it
finishes. The Agent Profiles list shows each saved profile’s status beside it —
signed in, signed out, credential expired, or the API-key and no-login
cases — so a signed-out profile is visible before a session fails. When Mjolnir
cannot inspect a login, the row says why instead (for example, Claude Code’s
macOS Keychain item). credential expired means the harness recorded an
access-token expiry that has passed;
some harnesses refresh it automatically on the next turn. The status is a local
credential probe, not a live provider check, so a token revoked by the provider
still reads signed in until a turn fails. The button is
available for profiles that already exist in the saved configuration and have no
unsaved edits; save a new or changed profile first.
When exactly one profile exists, --profile may be omitted. With several
profiles it is required. Mjolnir sets the selected home variable and profile
environment, installs the exact pinned harness under its own cache, and starts
that harness as an ACP agent. It reads the methods the bridge advertises:
| Harness | What mj login does |
|---|---|
| Codex | ACP authenticate for chat-gpt (browser) or api-key (environment key) |
| Claude Code | No ACP methods; runs auth login through the Claude CLI installed in Mjolnir’s pinned harness |
| Kimi Code | Runs its advertised ACP method; falls back to kimi login only when the bridge cannot run |
| Grok Build | Runs grok login for its grok.com method; falls back to the CLI when the bridge cannot run |
| Muse Code | Runs the ACP terminal method muse-acp login |
| OpenCode | Runs the pinned opencode auth login --standalone |
| DSH | none; mj login refuses the profile, which authenticates with DEEPSEEK_API_KEY |
Use --method <id> when a bridge advertises several methods. Use --cli to
skip ACP and run the harness command found on PATH, which is the behavior of
earlier releases. A bridge that advertises no usable method falls back to the
CLI in the same pinned installation. A failure inside a chosen ACP method is
reported instead of silently trying a different flow.
For harnesses with an ACP login, signing in installs or reuses the pinned
harness, so the controller does not need a separate installation of that
harness. Claude agent-acp advertises no login method, so Mjolnir runs
auth login through the Claude CLI in the pinned installation. A profile
pointed at your own OpenCode or Muse home signs in through that installed
harness’s CLI instead: Mjolnir never runs the pinned binary against a login
database it does not own. A profile with its own Mjolnir-managed home gets the
pinned flow. API-key profiles, and profiles whose harness is configured for an
API key rather than a native login, have no interactive login.
On macOS, Claude Code keeps its login in one Keychain item rather than in the
home (anthropics/claude-code#20553),
so mj login for a Claude profile leaves CLAUDE_CONFIG_DIR unset there and
signs in to that one item. Every Claude profile on a Mac shares it.
Mjolnir copies the current login into a session’s staged home when the session
runs in a container or on another machine, where the Keychain is unavailable.
The Keychain remains authoritative at each launch. A stored setup token takes
precedence. For a long-running container session, prefer the setup token because
the copied OAuth grant can rotate while both copies still hold the same refresh
token.
After login, Mjolnir compares the authentication marker before and after the command and reports whether it changed. A successful update is reconciled into live sessions while the daemon is running.
Claude long-lived setup token
Section titled “Claude long-lived setup token”Claude’s normal OAuth grant rotates. A controller and a copied session can race to spend the same refresh token when it expires. For long-running managed sessions, create a non-rotating setup token:
mj login --profile claude-work --setup-tokenThis runs setup-token through the Claude CLI in Mjolnir’s pinned installation,
verifies it with auth status, and stores it under Mjolnir’s configuration
directory at:
profiles/<profile-id>/claude-oauth-tokenNew and resumed sessions receive it as CLAUDE_CODE_OAUTH_TOKEN. It covers
model requests, not Claude Remote Control or claude.ai connectors. Remove that
file to return the profile to its normal synced credentials. --setup-token is
valid only for Claude profiles; it conflicts with --method and --cli.
What a session’s staged home holds
Section titled “What a session’s staged home holds”Every session runs from a staged home that belongs to it alone. On a bare
runtime on this machine it is workers/<session id>/profile under Mjolnir’s
data directory (profiles/<session id>/muse for Muse), in a container
/var/lib/hel/profiles/<session id>, and on an SSH machine or EC2 instance
~/.local/share/hel/profiles/<session id>. The harness’s home variable, such as
CODEX_HOME or CLAUDE_CONFIG_DIR, points at it, on macOS as elsewhere.
Closing the session removes it.
Mjolnir does not copy an entire home directory. It copies only this allowlist from the profile home:
| Harness | Staged home entries |
|---|---|
| Codex | auth.json, config.toml, AGENTS.md, instructions.md, rules/, skills/ |
| Claude Code | .claude.json, .credentials.json, settings.json, CLAUDE.md, skills/, plugins/ |
| Kimi Code | credentials/, config.toml, device_id, AGENTS.md, SYSTEM.md, mcp.json, skills/, agents/, plugins/ |
| Grok Build | auth.json, config.toml, AGENTS.md, agent_id, skills/, plugins/ |
| Muse Code | auth.json, settings.json, trust.json, AGENTS.md, skills/, rules/ |
| OpenCode | opencode.json, opencode.jsonc, AGENTS.md, skills/, and the logins from opencode.db, as .data/opencode/mjolnir-credentials.json |
| DSH | AGENTS.md, skills/ (staged, but DSH does not read skills) |
Staging follows symbolic links: a linked file or directory is copied with the contents of its target, even when the target is outside the harness home. A link whose target is missing is skipped with a warning, and a link back into a directory already being copied is skipped, so a loop is copied once.
Claude Code’s own skills/synced/ and skills/.trash/ directories, and
Codex’s own skills/.system/, are not copied; see
Skills synchronization.
Mjolnir then adds its own files to the staged home:
- on localhost, the managed
mjskill (see Managed skills); - the session’s replica of its project memory, under
projects/; - for a Claude session with Mjolnir sub-agents, or a Claude sub-agent, the
mj-agentsMCP server in.claude.json, and an allow rule insettings.jsonfor each of its tools, so Claude never asks before a sub-agent hands back its report or a parent starts or waits for one; - for a Codex profile with a profile file, that file
merged into
config.toml; - for a Codex profile with a custom provider, the generated
models.jsonand theconfig.tomlline that points at it; - for Kimi Code on a target other than this machine, the
mj-memoryhistory MCP server inmcp.json; - for Muse, the permission profile in its settings;
- for OpenCode under an unconstrained policy, the
"permission": "allow"setting inopencode.json; - for every OpenCode session, permission rules in
OPENCODE_CONFIG_CONTENTthat turn off OpenCode’sexecutetool, so themj-agentstools are ordinary tools namedmj-agents_spawnand so on instead of reachable only through scripts, and, unless the session uses OpenCode’s own delegation (Native), its ownsubagenttool. OpenCode names a tool<server>_<tool>, so another server can carry the same name as a Mjolnir tool; the worker therefore never answers OpenCode’s permission requests, and under configured approvals the person approves Mjolnir’s tools too; - for DSH, the staged
AGENTS.mdin the session’sDSH_ACP_DATA_DIR(withDSH_ACP_SYSTEM_PROMPTcleared), and dsh-acp’spermission_modeon every session open:askunder configured approvals,allowunder an unconstrained policy; - in a container or on an EC2 instance, a note in the instruction file
(
AGENTS.mdorCLAUDE.md) that the environment is disposable.
History, caches, SSH and GPG keys, shell dotfiles, cloud configuration, editor state, and package-registry credentials are not copied merely because they sit under your user home. The profile home’s own native history is not copied either, and nothing Mjolnir adds is ever written to the profile home. A staged home isolates agent state; it does not sandbox a raw local process, which runs as your account and can read your whole home directory.
Sessions from earlier releases
Section titled “Sessions from earlier releases”Earlier releases ran a local session of Codex, Kimi Code or Grok Build, and a Claude Code session on macOS, from the profile home itself. Such a session that is still open keeps running from there until it is suspended and resumed, or moved, which stages it like any other. Until then its credentials and skills are not synchronized.
Those sessions also left their project-memory replicas under projects/hel-* in
the profile home. Mjolnir does not remove them: several Mjolnir instances can
share a profile home, and one instance cannot tell another’s stopped session
from an ended one. The project memory itself is kept in Mjolnir’s data
directory, so you can delete a leftover projects/hel-* directory by hand once
its session is closed. In a Claude Code project directory, keep the session’s
native transcripts.
Credential bytes travel only in direct controller-to-worker messages. They are excluded from the durable event journal and recovery archives. Fingerprints and freshness timestamps may appear in logs; credential contents do not.
Native session history
Section titled “Native session history”A harness writes its own record of a session, such as a Codex rollout under
sessions/ or a Claude Code transcript under projects/, into the home it runs
from. For a session Mjolnir runs, that is the session’s staged home, not your
profile home, on this machine as on any other target. Mjolnir includes the
native record in the session’s checkpoints and removes it with the session.
What this means:
- Your harness’s own resume command, such as
codex resumeorclaude --resume, does not list sessions that Mjolnir runs. Find and resume them through Mjolnir instead: the Mjolnir and Archived tabs of the session dialog (prefix+g),mj resume, orsessionwiki search. - The Import tab and
mj importread your profile homes, so they list only sessions you ran outside Mjolnir.
Credential reconciliation
Section titled “Credential reconciliation”The daemon reconciles every profile with its live sessions about once per minute and may trigger an immediate pass after an authentication failure. Every session, including one on this machine, has its own copy of the login in its staged home. Normally the controller-side profile home is canonical and replaces older session copies. If a rotating login becomes fresher inside a session, that copy can become canonical and then propagate to sibling sessions. Several sessions of one profile therefore hold copies of one rotating grant, just as container sessions do.
Codex, Kimi Code, and Muse Code logins are refreshed ahead of expiry when possible. For Claude, prefer the long-lived setup token above. A reconciliation failure is surfaced rather than silently discarded.
GitHub authentication is separate from harness authentication. Mjolnir reads
GH_TOKEN, then GITHUB_TOKEN, then gh auth token --hostname github.com. It
pushes the active token to every live non-local session, including raw SSH, so
HTTPS Git and gh work without copying SSH keys. Raw localhost is excluded.
The GitHub token is also excluded from checkpoints and archives.
Skills synchronization
Section titled “Skills synchronization”Every supported harness except DSH, which has no skills support, resolves user
skills from skills/ beneath its profile home. Mjolnir treats the controller
copy as authoritative and pushes it to live sessions on the same reconciliation
cycle. This direction is deliberate: a
session cannot overwrite the canonical skills tree on your machine.
Claude Code and Codex also keep skills of their own under skills/. Claude
Code provisions skills/synced/ from your claude.ai account and moves skills it
removes into skills/.trash/. Codex writes its built-in skills into
skills/.system/. These directories belong to the harness, which writes them
in whatever home it runs from, including a session’s home. Mjolnir does not
stage, compare, or push them, and it never replaces or removes them in a
session.
The sync reads the controller-side skills/ the way launch staging copies it.
It follows symbolic links, leaves out a link whose target is missing, and reads
a directory that links back into itself once, so a linked skill is part of both
the staged and the synced tree. It writes only into the session’s staged home,
never into your own skills/ directory.
The whole tree travels as one gzip-compressed archive, base64-encoded, in a single 8 MiB relay frame. The size limits apply to compressed sizes:
- 1 MiB per file, compressed (
MAX_SKILLS_FILE_BYTES). Mjolnir measures a file by compressing it on its own; a file of 1 MiB or less always fits. A larger file that is still over 1 MiB once compressed, or a file that cannot be read, is left out of the sync, and the rest of the tree still syncs. The daemon and each session’s worker log a warning the first time they skip a file, with the file in itspathfield:skills file is <size> bytes and compresses to <compressed> bytes, above the 1048576 byte limit; leaving it out of skills sync. Later skips are logged only at debug level. Launch staging has no file size limit, so a new session starts with the file; the next push of a changed tree removes it from the session. - 4 MiB per tree, compressed (
MAX_SKILLS_ARCHIVE_BYTES), counting file contents, paths, and a few bytes per file. Base64 makes 4 MiB about 5.3 MiB, which leaves room in the frame for the rest of the message. - 64 MiB per tree, uncompressed (
MAX_SKILLS_TREE_BYTES). This limit keeps a small archive from expanding into more memory than a worker should use. Real skills reach the compressed limit long before it. A single file larger than this is left out without being read. - 1024 files per tree (
MAX_SKILLS_FILES).
In practice, text compresses well, so skills with several megabytes of Markdown, HTML, or scripts fit. For example, a 2.2 MB HTML demo page compresses to about 300 KB. A checked-in binary, such as an image, an archive, or a model file, hardly compresses at all, so one over 1 MiB is still left out.
Directories a harness keeps for itself do not count toward these limits. A tree over either per-tree size limit, or with more than 1024 files, is not trimmed: reconciliation fails for every session of that profile, credential sync included, until the tree is back within the limits.
A session whose worker comes from a release before compressed archives (relay
protocol 23) reads only the uncompressed format. Until that worker is replaced
at the session’s next quiet point after the upgrade, Mjolnir sends it the tree
uncompressed, with the limits counting raw sizes: 1 MiB per file and 4 MiB per
tree. A file over 1 MiB is left out of that session, and the daemon’s warning
says it is above the 1048576 byte limit of an uncompressed skills archive.
Managed skills
Section titled “Managed skills”Mjolnir installs the mj skill into localhost sessions. You can ask the model
to “configure mj with …”; the skill explains editing your Mjolnir
configuration and includes the configuration reference.
It also teaches session orchestration through the mj CLI. Localhost agents
receive the owning daemon’s configuration and data paths so their commands
address the same Mjolnir instance.
Container, SSH, and EC2 sessions do not receive this host CLI skill. Their
delegation and session-history MCP tools carry their own instructions. Launch
staging and ongoing skills reconciliation both enforce this distinction,
including removing an older copy from an isolated session. The skills/mj/
directory is reserved: localhost sessions receive the managed copy and isolated
sessions omit it, even if the profile contains a user copy. Your own skills/
directory is left alone.
Session recall and file provenance are provided by the mj-memory MCP server:
search_sessions, get_session_brief, search_session, read_session,
trace_file, session_files, and blame_file. They query the controller’s
session index on local, container, and SSH targets without installing sw or
copying the index to the target. Claude receives these history tools and keeps
native project notes. Other harnesses read and write their session’s project
memory replica with their own file tools. Applicable history tools are
registered again when a session resumes.
History reads are bounded and include continuation information. The index can
lag active sessions, and older transcripts may lack timestamps or file evidence.
blame_file runs Git in the target checkout and reports heuristic attribution;
uncommitted lines remain unattributed. Queries require a connected controller
and time out after 60 seconds. Local project-memory files remain usable while a
history query waits. History tools never resume or restore old sessions.
User-supplied recall and provenance skills are preserved.
The destination tree is replaced atomically. Removing the controller-side
skills/ directory therefore removes the synced tree on the next successful
reconciliation. Other allowlisted directories such as harness plugins are
staged when a session is created but are not part of this continuous skills
sync.
Harness runtimes
Section titled “Harness runtimes”Mjolnir talks to harnesses through the Agent Client Protocol (ACP). The published agent image already carries the supported bridge stack. Container targets use that target-provided runtime.
Local-bare, raw SSH, and EC2 workers instead install the exact versions pinned by the
Mjolnir release into $XDG_CACHE_HOME/mjolnir/harnesses, or
$HOME/.cache/mjolnir/harnesses when XDG_CACHE_HOME is unset. They launch
only the resulting absolute path—never an arbitrary compatible executable from
PATH. DSH is the exception and is never installed; see below.
For Codex, Claude, Muse, and DSH’s npx fallback, Mjolnir uses Node.js 22
or newer with npm and npx when the session PATH already provides that stack.
Otherwise it downloads the pinned Node.js 24.22.1 distribution with its internal
HTTPS client, verifies its published SHA-256 checksum, and installs it under
$XDG_CACHE_HOME/mjolnir/node (or $HOME/.cache/mjolnir/node). The managed
bin directory is prepended for the harness and its child processes. A target
that mirrors the Node.js distribution can set NODEJS_ORG_MIRROR (or the
compatible NVM_NODEJS_ORG_MIRROR) to an HTTPS base URL; Mjolnir still checks
the pinned digest.
Kimi Code and Grok Build install from their direct per-platform release
artifacts, with pinned SHA-256 checksums. Mjolnir launches the pinned
muse-acp adapter through npx and downloads the pinned native Muse binary,
verifying its SHA-256 checksum. Linux and macOS, on x86-64 and ARM64, are
supported. The native Muse binary retains its own upstream terms.
OpenCode downloads the pinned OpenCode 2 package for the platform from npm
(@opencode/cli-<platform>), the same package OpenCode’s own installer fetches,
and verifies it in-process. Linux and macOS, on x86-64 and ARM64, are supported.
The pinned installation disables OpenCode’s self-update so a running session
never replaces its own binary.
DSH is never installed. A launch uses the target’s own dsh-acp on PATH when
it is 0.5.0 or newer, or fetches the pinned @brokkai/dsh-acp@0.5.0 package
through npx, using the compatible system runtime or the managed Node
described above; the npm package bundles the release binaries. An older
dsh-acp on PATH is not used: Mjolnir falls back to npx when Node.js, npm,
and npx are available, and otherwise refuses the launch with an upgrade
instruction. Linux and macOS, on x86-64 and ARM64, are supported. Linux needs
glibc 2.34 or newer, so musl distributions such as Alpine cannot run it.
Mjolnir reports a missing prerequisite and leaves the existing worker alone; it
does not invoke sudo or a system package manager.
Installs are content-addressed and shared across sessions for the same remote user. A cache hit performs only local manifest and executable checks. Upgrades prepare a new version before replacing a quiet worker. Old versions remain leased for the complete ACP process lifetime—including busy turns that last hours—and are garbage-collected only after the final user exits. For custom container images, see Custom images.
Profiles pane
Section titled “Profiles pane”The dashboard asks every configured profile for current capacity and refreshes
profiles independently, so a slow provider does not delay the others. Press
prefix+shift+r to refresh Targets and Profiles immediately.
The pane’s title, Profiles ▾, opens a small menu. Click it, or press .
while the pane has focus. Refresh runs the same refresh as
prefix+shift+r, and Settings… opens the Agent Profiles page of Settings.
| Harness | Quota source shown by Mjolnir |
|---|---|
| Codex | Provider usage windows and reset times. For a custom provider, the provider’s own windows when it publishes them. |
| Claude Code | Five-hour and weekly subscription windows when reported. |
| Kimi Code | Usage windows returned by the configured Kimi service. |
| Grok Build | The harness’s ACP billing extension. |
| Muse Code | Native subscription usage windows and reset times, when reported. The daemon refreshes the Muse login itself, so the reading does not depend on Muse having run. |
| OpenCode | No published quota endpoint; the profile shows an unavailable reading. |
| DSH | No quota endpoint; DeepSeek bills by usage, so the profile shows API. |
A Codex profile on a provider that publishes no quota endpoint, and a DSH
profile, show API instead of a window, because they are usage-priced. An
unavailable reading is displayed as an error for that profile; it does not make the profile
disappear. Quota is advisory rather than an admission-control scheduler. Session creation remains your decision.
Mjolnir may use a currently available non-Claude profile for internal utility work such as transcript compaction when appropriate. It does not silently move the primary coding session to another profile.
Harness limitations
Section titled “Harness limitations”Muse supports streamed chat and tools, images, model and effort selectors,
approval questions, cancellation, and native resume. /plan and /implement
switch muse-acp’s Plan mode, which moves the session to a Muse host that cannot
write files or run shell commands; plan approval arrives as chat text rather
than a choice dialog.
Muse decides a session’s permission profile from its settings file, so
Mjolnir writes it into the staged Muse settings. With guardian approvals it
stages :ask-me, keeps Muse’s sandbox on (removing --disable-sandbox from a
profile’s MUSE_SERVE_ARGS), selects promptUnmatched
approvals, and turns on muse-acp’s auto-review for every session: a second,
read-only Muse model reviews each approval request with a Codex-style safety
policy, and a failed or unusable review denies the action. Muse’s own
:auto-review profile is never staged, because muse serve refuses it.
Unconstrained sessions stage :ask-me as well, but select allowAll
approvals with auto-review off and disable only the sandbox
(--disable-sandbox), so they run without permission checks like OpenCode.
A Muse profile with muse_auto_review = true keeps the reviewer on in
unconstrained targets too (promptUnmatched approvals with auto-review on
inside the container boundary).
Muse accepts one workspace root, without attached directories. Native import
and checkpoint restore can relocate that workspace while retaining the session
identity. Archives include the selected session and its child streams, not
other sessions or credentials. External Muse sessions normally come from
~/.local/share/muse/sessions (XDG_DATA_HOME/muse/sessions when set); mj
restores them into the destination profile’s isolated data directory.
Unconstrained Muse reads and writes its session’s project-memory files directly because its sandbox is disabled; with guardian approvals, writes outside the workspace go to auto-review. It receives Mjolnir’s session-history and review MCP tools through muse-acp 0.8.0 or newer with Muse Code 1.3.0 or newer, which managed targets and the agent-dev image install. A session whose Muse runtime does not accept MCP servers, such as one in a container created from an older image, continues without those tools and says so; suspend and resume it once the image has updated. A Muse reviewer needs its review MCP tools, so it does not start without them. Muse Spark can also supply utility inference for cross-harness handoffs; see Durability and recovery.
- Kimi Code, OpenCode, and DSH have no guardian approval mode. Prefer an isolated container target or EC2 rather than raw execution.
- OpenCode talks to third-party providers through its own gateway, so it
publishes no quota endpoint and its profile shows quota as unavailable. It
reads permission rules from
opencode.json. Its sessions live in a SQLite database, so native import and SessionWiki search do not list them, and it never serves Mjolnir’s delegation or utility inference. - DSH has no sandbox: commands run directly on the host, and only its file
tools are confined to the workspace roots. Under configured approvals
Mjolnir selects dsh-acp’s
permission_modeaskon every session open, so every write, command, and MCP tool call asks for permission, except calls to Mjolnir’s ownmj-agentsandmj-memorytools, which the worker approves without asking, as Claude and Codex sessions do; an unconstrained policy selectsallow. The Plan toggle switches dsh-acp to itsplanmode and back to itsagentmode. DSH accepts multiple workspace roots and attached directories. Checkpoints keep the session’ssessions/<id>.json, and a restore at a different workspace path rewrites the record’s working directory so the session resumes. Native import and SessionWiki search do not support DSH yet. A DSH session can use Mjolnir’s sub-agents, and a DSH profile can run them: it reads the delegation policy from its stagedAGENTS.mdand receives themj-agentstools over ACP. dsh-acp has no sub-agents of its own, so DSH offers Mjolnir and None but not Native. A DSH session stored with Native keeps working and behaves as None. It serves DeepSeek-family utility inference with its flash model only when its base URL is DeepSeek’s ownapi.deepseek.comand its key is set. - A custom bridge must speak the ACP version and features Mjolnir expects.
- A profile home is account-scoped. Do not point two profiles at the same home and expect them to represent different accounts.
- Profile
environmentis stored as plain text inconfig.toml; do not use it as a general secret store.
Continue with Session lifecycle to see how profile, model, effort, plan mode, and resume choices interact.