Skip to content

Troubleshooting

Start with Mjolnir’s own diagnosis. Its checks share the same configuration and target validation code used during setup and launch, so their remediation is more useful than a generic package checklist.

Run the machine-readable report:

Terminal window
mj doctor --json

Then:

  1. Follow every check whose status is fixable.

  2. Run mj doctor --json again.

  3. Repeat until no check is fixable.

  4. If you use containers, finish with an end-to-end disposable target test:

    Terminal window
    mj doctor --json --smoke

--smoke goes beyond static prerequisite checks for Podman, Docker, and Apple Container, on this machine or over SSH: it launches, executes in, and removes a disposable container. It does not smoke-test a bare runtime on any machine. The Docker test also proves the writable OverlayFS attachment path and cleanup.

For a self-contained handoff to another coding agent, generate the platform instructions and include the latest JSON report:

Terminal window
mj setup instructions --platform linux
# or
mj setup instructions --platform macos

The human-readable mj doctor output is convenient at a terminal; JSON is better for exact check IDs, automation, and issue reports. A warning can still matter, but only fixable means the doctor loop has not converged.

Configuration is missing, invalid, or from a newer build

Section titled “Configuration is missing, invalid, or from a newer build”

If config.toml does not exist, run mj and open prefix+s Settings to add or detect an agent profile. Local target choices are supplied automatically. Managed container and EC2 sessions also need a bundle; a bare session selects an existing Git project directory instead.

For a TOML error, fix the exact path and type named by mj doctor. Mjolnir’s schema rejects unknown fields rather than silently ignoring a misspelling. See the Configuration reference for every field.

An older Mjolnir refuses to load a configuration last written by a newer build. Update Mjolnir, or use the newer build; do not lower the version value by hand.

A bare clone does not contain my local changes

Section titled “A bare clone does not contain my local changes”

When Create isolated checkout is checked for a new bare session, Mjolnir creates an independent clone. Source changes do not enter that clone. If you expected to see uncommitted files from the selected directory, inspect them there:

Inspect the selected checkout on the local or remote target:

Terminal window
git -C <project-directory> status --short --untracked-files=all

For a network-backed clone, commit and push the work and select the branch that contains it; the default is the remote’s default branch. To work directly in the selected directory with its current changes, uncheck Create isolated checkout on the final review. See Targets and Workspaces and bundles.

Run the login through Mjolnir so it uses the configured profile home:

Terminal window
mj login --profile <profile-id>

If exactly one profile exists, --profile is optional, but naming it removes ambiguity. Then rerun mj doctor --json. If doctor still reports no usable authentication:

  • confirm that [profiles.<id>].home points to the home used by that harness account;
  • make sure the harness login completed and wrote its normal authentication marker beneath that home;
  • check that the profile kind matches the files in the home; and
  • resolve any executable or PATH failure before repeating login.

A DSH profile has no login, and mj login refuses it. It authenticates with DEEPSEEK_API_KEY, taken from the environment Mjolnir started with unless the profile’s environment sets it; mj doctor and the refused session say how to supply it. Export the key before running mj, or run mj daemon restart after exporting it in a new shell.

For unattended or concurrent Claude Code sessions, prefer a long-lived setup token:

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

It avoids competing session copies trying to rotate the same single-use OAuth refresh grant. Credential behavior and its limits are explained in Security boundaries; profile fields are in Profiles and harnesses.

GitHub authentication fails inside a target

Section titled “GitHub authentication fails inside a target”

Check the controller first:

Terminal window
gh auth status

Mjolnir synchronizes the active gh token into live non-local sessions. It does not copy SSH keys, and raw local sessions are excluded from this token sync. Authenticate gh on the controller, wait for the next synchronization, and use HTTPS Git URLs in isolated targets.

For a repository configured with local, isolated sessions resolve its default network fetch and push remotes. Check those URLs and the fetch remote’s advertised default branch. Local unpublished commits and dirty files are not copied, so cleaning the host working tree does not fix a remote-authentication failure. Repositories without network remotes require a raw local session. Legacy host-bridge checkpoints cannot resume; start a new network-backed session. See Workspaces and bundles.

The harness exists in a shell but Mjolnir cannot launch it

Section titled “The harness exists in a shell but Mjolnir cannot launch it”

Mjolnir starts the account’s shell once per worker to learn the environment a session should have, and it starts it the way a terminal does: as an interactive login shell. The startup files a terminal reads — ~/.zprofile and ~/.zshrc for zsh, ~/.bash_profile and ~/.bashrc for bash — are read too, so a tool that a version manager such as nvm, pyenv, asdf or mise puts on PATH is visible to sessions.

Two things still differ from your terminal. A shell alias or shell function is not part of the environment, so a tool that exists only as an alias or function cannot be launched by a session. And a worker keeps the environment it discovered when it started, so after changing a startup file, stop and resume the session rather than expecting a running worker to reread it.

mj doctor reports which shell sessions start, how long that startup took, and the node, npm and npx a session will find. Use it when a tool works in your terminal but a session cannot launch it.

An explicit profile setting wins over discovery:

[profiles.my-profile.environment]
PATH = "/opt/my-tools/bin:/usr/local/bin:/usr/bin:/bin"

After changing a profile’s environment, restart the worker or stop and resume the session; an already running worker does not continuously reread shell startup files. Bare sessions on this machine, SSH, and EC2 do not launch an ambient harness from this path: they use the exact Mjolnir-managed version, while PATH supplies its installer prerequisites. These targets require Node.js 22 and npm for Codex, Claude, and MiMo Code, curl and Bash for Kimi and Grok, or curl and tar for Muse, OpenCode, and DSH. Install prerequisites on the host yourself; Mjolnir never invokes sudo for harness setup.

Container images must expose required tools on their ordinary image PATH. Agent-requested ! shell commands use bash -lc, so their environment can differ from the worker and bridge environment. That distinction explains the common case where !which tool succeeds but session launch says the tool is missing. See Custom container images and the profile fields in Configuration reference.

Mjolnir’s global commands run from a tmux-style prefix key (ctrl+b by default): press it, release it, then press the command letter. If pressing it does nothing, something between your keyboard and Mjolnir is intercepting it:

  • tmux or GNU screen, run with their own default prefix (ctrl+b for tmux, ctrl+a for screen), consumes the key before Mjolnir sees it. Press it twice to send the literal key through, the same way you would reach any other application’s own binding on that key from inside the multiplexer; or set a different prefix in [keys] so the two tools stop colliding.
  • A terminal emulator or desktop environment can also bind the same key globally; check its keyboard shortcut settings if the multiplexer explanation does not apply.

Plain ? still opens help while focus is outside Prompt, independent of the prefix. See Terminal surface for the full default table and Configuration reference for rebinding the prefix.

An invalid [keys] entry is a configuration error like any other, and it names the exact field. At startup this is fatal: Mjolnir refuses to run until the file is fixed. While Mjolnir is already running, the daemon reloads config.toml continuously but keeps the last good configuration and only logs a warning when a reload fails, so a broken edit can look like nothing happened rather than like an error. Run mj doctor to see the current validation error, or restart Mjolnir to have it reported at startup.

Run the smoke test before changing the Mjolnir configuration:

Terminal window
mj doctor --json --smoke

Then follow the target-specific result:

  • Podman: it must be rootless and have a valid subordinate UID/GID mapping. Run podman info --format json as the same user that runs Mjolnir and check that host.security.rootless is true and host.idMappings.uidmap covers container IDs 0 and 1, then follow Podman for Mjolnir. On macOS the podman client talks to a Linux VM (podman machine), which is supported when the machine is rootless. If it is rootful, run podman machine set --rootful=false and restart it. If podman info cannot connect, run podman machine start.
  • Docker: the CLI must reach a Linux daemon, and writable attachments need working OverlayFS support on the daemon’s filesystem host. Run docker info and follow Docker for Mjolnir.
  • Apple container: the controller must be Apple silicon on macOS 26 or newer. If doctor reports a stopped service, run container system start. See Apple container.
  • SSH Podman: run the Podman prerequisite and image checks on the remote host as the same user selected by SSH. See SSH and SSH Podman.
  • EC2: verify the configured AWS profile, region, launch template, SSH user, address source, and reachability. See AWS EC2.

If the runtime works but the harness bridge does not, the image likely lacks a required binary, Node version, writable Mjolnir directories, or ordinary process PATH. Compare it with the contract in Custom container images.

This is expected when Mjolnir detects a filesystem that cannot safely host its copy-on-write overlay, including NFS, SMB, FUSE, FAT-family filesystems, and an already layered OverlayFS. The launch notice names the detected filesystem.

Move the source to a compatible local Linux filesystem if writes are required, or keep it read-only and write results into the session workspace. A writable copy-on-write attachment never writes back to the original source. See Container targets.

Start with:

Terminal window
mj daemon status

The status includes daemon version and PID, viewer state, URL, login code, and the reason for any loopback fallback. Then check these cases:

  • If the viewer is disabled, set phone.enabled = true and run mj daemon restart.
  • Without trusted TLS, the viewer intentionally serves only on http://127.0.0.1:3765 by default. It is not reachable from another device.
  • Automatic tailnet access requires Tailscale MagicDNS and HTTPS Certificates. Enable them, then run mj daemon restart. First certificate issuance can take about 30 seconds.
  • Explicit TLS requires both phone.tls_cert and phone.tls_key. Mjolnir refuses a non-loopback plaintext bind and a half-configured key pair.
  • Five incorrect six-digit codes trigger a 30-second lockout; repeated lockouts grow to one hour. Wait for the reported lockout rather than retrying continuously.

The native mj app command opens the same viewer. On Linux its separate desktop executable needs the system WebKitGTK 4.1 runtime—for Debian and Ubuntu the package is commonly libwebkit2gtk-4.1-0. The headless mj controller does not need that library.

See Web viewer and desktop app for setup, Security boundaries for its authentication model, and Configuration reference for [phone].

Automatic checkpoints and worker replacement require a quiet boundary. A session is not quiet while any of these remain:

  • an active user prompt or autonomous harness turn;
  • a queued prompt or configuration change;
  • a user shell command;
  • an agent-owned terminal or background command; or
  • a checkpoint barrier already in progress.

Inspect the selected session and its queue. Cancel only work you are willing to interrupt; a background command can be real agent work even after the last visible response. Once all work ends, the recovery and upgrade coordinators observe the idle state automatically. Read Durability and recovery for the exact guarantees.

Suspension failed or a session needs recovery

Section titled “Suspension failed or a session needs recovery”

A normal suspension will not destroy a target unless its recovery archive passes the checksum and frontier gates. A suspension failure is therefore normally non-destructive: read the exact checkpoint or target error, repair the storage or connection problem, and try a manual checkpoint:

Terminal window
mj checkpoint --session <session-id>

Then choose Retry suspension in the failure dialog, or run Suspend session… again from the command palette. If a fresh checkpoint remains impossible but an older verified archive exists, the failure dialog also offers Discard changes since checkpoint…. That removes the live target without a new archive and can lose every change after the older checkpoint, but leaves the session resumable. Destroy session… is different: it removes the target, archive, and record permanently.

If a managed target is still running but has disappeared from controller state, scan without changing it:

Terminal window
mj recover scan --json

Adopt the exact resource you recognize:

Terminal window
mj recover adopt --session <session-id> --target <target-id>

Resources created before current ownership markers may also need --profile and --bundle. Adoption reconnects to the existing worker and its newer on-target journal; resuming only from an archive may be older.

Destroy an untracked resource only after confirming it is the right one and that no newer work needs recovery:

Terminal window
mj recover destroy \
--session <session-id> \
--target <target-id> \
--confirm <session-id>

The confirmation value must repeat the full session ID. For stopped, lost, or retryable sessions already present in the dashboard, use the resume and recover actions described in Session lifecycle.

When the built-in remediation is not enough, collect:

  • mj --version, operating system, architecture, harness profile ID, and target ID;
  • the latest mj doctor --json output;
  • the exact launch, checkpoint, resume, or viewer error; and
  • the newest mj-daemon-*.log under Mjolnir’s platform data directory logs/ subdirectory, which is the long-running daemon’s own log; mj-tui-*.log and mj-cli-*.log files there are shorter-lived dashboard and one-shot command runs, retained separately.

Set MJ_DATA_DIR only when you intentionally override that platform data directory. Worker connection failures include the worker exit record and the tail of worker.log when Mjolnir can still reach the target, so preserve the complete error chain.

Do not attach raw harness authentication files, GitHub tokens, TLS private keys, recovery archives, or a full profile home to an issue. Paths and non-secret credential fingerprints can still be identifying; review diagnostic output before sharing it.