Skip to content

Workspaces and bundles

Workspaces and bundles solve different problems:

  • A workspace is a named control-plane view. It groups live sessions, drafts, and read state for one operator context.
  • A bundle is a configured project shape. It tells a managed target which repositories to check out and which one is the agent’s primary working directory.

They are deliberately independent. The same bundle can be used by sessions in several workspaces, and a workspace can hold sessions from several bundles. Persistent project memory follows the project identity, not the workspace name.

Run mj to open a workspace. If none exists, Mjolnir creates one using the current directory name, then leaves the dashboard ready for an explicit new session. Otherwise it opens the requested workspace or the most recently opened workspace. Use either of these forms when you want to choose explicitly:

Terminal window
mj workspaces
mj --workspace "Release work"

--workspace matches names case-insensitively. In the terminal surface, a bordered three-row Workspaces pane sits above Sessions. Its right-hand ≡ button opens the workspace manager. From the keyboard, focus the Workspaces pane, move from the tabs to ≡ with Tab or Right, then press Enter; Shift-Tab from the Sessions pane also lands on it. The command palette also lists Workspaces (prefix+shift+n). Selecting a tab, or pressing an arrow while the workspace tabs have focus, changes the live-session filter immediately. Tabs are local views, so sessions in other workspaces continue running independently. The web viewer shows each workspace as a separate tab. The terminal surface needs at least 60 columns.

The manager can create, rename, and delete workspaces and recover drafts. Names are trimmed, must be 1–64 Unicode characters, cannot contain control characters, and are unique case-insensitively. Release work and release work therefore name the same workspace. Deleting the last workspace leaves the manager open; Mjolnir does not create a session automatically.

A script does not need the dashboard. mj workspaces list prints the workspaces, and mj workspaces create <name> creates one — or selects the one that already carries the name, so it is safe to run before every session. Both are thin clients for GET and POST /api/v1/workspaces; see the HTTP API reference.

mj new requires --workspace <name> or --workspace-id <id>, even when there is only one workspace. A fresh instance has none; create one first:

Terminal window
mj workspaces create agents
mj new --workspace agents --project-directory . "Review this project"

The HTTP create-session endpoint can infer the workspace only when exactly one exists. Neither interface creates an implicit default workspace for a new session. Plain mj still creates its first dashboard workspace from the current directory name.

A workspace owns the active presentation of:

  • live sessions and their selected order;
  • per-client read frontiers;
  • detached terminal drafts; and
  • per-browser conversation drafts.

Read state and browser drafts are client-specific, so opening a session on your phone does not consume another terminal’s unread marker or steal its unsent text. The daemon remains the owner of the actual sessions.

Suspended histories are global resume candidates. Resuming one moves it into the workspace from which you resume, even if its former workspace was deleted. This is why deleting an otherwise empty workspace does not erase stopped session history.

Deleting a workspace first suspends its active sessions, preserving verified recovery copies and stopping their Mjolnir sub-agents. The confirmation also discards detached drafts and unsent composer text. Only after every suspension succeeds does Mjolnir remove the workspace and its drafts.

If a suspension fails or deletion is cancelled, the workspace and drafts stay; sessions already suspended remain suspended. Retry after resolving the failure. Retained session histories remain available in the global resume picker and can be resumed into another workspace. See Durability and recovery for session-level destruction and recovery guarantees.

Workspaces are stored in mj.sqlite3, not config.toml. Do not add a [workspaces] table to the configuration file.

Container, SSH Podman, and EC2 targets start from a bundle. A bundle can contain one repository or assemble several repositories into a virtual monorepo. Its primary_repo becomes the ACP session working directory; every other member is provided to the harness as an additional workspace root.

[bundles.product]
primary_repo = "app"
[[bundles.product.repositories]]
id = "app"
github = "acme/app"
destination = "app"
[[bundles.product.repositories]]
id = "shared"
local = "/home/me/src/shared"
destination = "shared"

This produces a target workspace conceptually like:

<workspace-root>/
├── app/ ← primary working directory
└── shared/ ← additional workspace root

Bundle and repository IDs use 1–64 ASCII letters, digits, ., -, or _; . and .. are invalid. Repository IDs must be unique within the bundle, and primary_repo must name one of them.

Each repository declares exactly one source:

  • github accepts owner/repository, a GitHub HTTPS URL, or a supported GitHub SSH URL.
  • local names an absolute controller-side Git repository whose network remote configuration is resolved at session creation. Its local commits and working tree are never copied into an isolated session.

destination is a non-empty relative path beneath the bundle root. It cannot contain . or .., and two destinations cannot overlap. For example, api and api/generated cannot coexist because one would contain the other.

See the Configuration reference for every accepted source form and validation rule.

In a new terminal or web session, choose a saved project or browse for a repository. In the terminal, Add project opens the browser for a project that is not saved. You do not need to create or name a bundle first. The project picker offers:

  • Recent: pick a project you have worked on before.
  • GitHub: browse repositories available to the GitHub account already signed in on the computer running Mjolnir, or search by name. If needed, the picker explains how to install or sign in to GitHub CLI on that computer.
  • Folders: open folders, go up a level, and choose a Git repository without typing its path. These folders belong to the computer running Mjolnir, not the phone or browser device.
  • Paste URL: enter a GitHub URL, owner/repository, or an existing local repository path.

Choose a repository to continue directly to the session review. Mjolnir saves it for next time. In the terminal, Use several repositories together lets you select several projects. In the browser, choose Select multiple repositories, add repositories from GitHub, folders, or a pasted URL, then choose Use project. The first repository is primary, where the agent starts; removing it makes the next repository primary. Existing multi-repository configurations remain available as saved projects. Edit config.toml for a custom repository layout.

Projects saves the complete repository set, primary repository, and layout. A subdirectory, symlink, linked worktree, and matching GitHub URL resolve to the same repository. Identity follows the fetch remote tracked by the default branch, so switching feature branches or using a separate push remote does not change the project. Selecting a single repository never adds other members of a larger saved project.

On first discovery, Mjolnir reads project directories from the ten newest native sessions in each configured harness home. It does not import those conversations. Later openings of Projects or the raw directory picker refresh from new Mjolnir session records. Unavailable directories are reported, other homes continue, and Retry discovery retries failed candidates. Local resolution uses Git; GitHub CLI remains necessary only for the existing GitHub browser/authentication integration.

Equivalent saved definitions are consolidated. Existing sessions retain their accepted repository IDs, layout, and fetch/push settings, and their history stays available through the old project IDs. Custom layouts and different primary repositories remain distinct saved projects.

Isolated sessions begin at the fetch remote’s default branch. Local unpublished commits and working-tree changes are excluded; the review shows the source before launch. git_ref is obsolete and is rejected with migration guidance.

Bare runtimes work differently. A new bare session, on this machine or on an SSH machine, selects an existing absolute Git project directory instead of a configured bundle. Its directory is represented internally as a project with one repository; the selected directory remains the session’s working directory. Its history and memory share repository identity with a managed project of one. The final review offers Create isolated checkout, on by default for a primary checkout and off for an existing linked worktree. When checked, Mjolnir creates a session-specific independent clone under .mj/clones/ and leaves source working-tree changes behind. When unchecked, the session uses the selected directory directly. See Targets.

GitHub members clone the default branch of their configured source. For private HTTPS repositories, Mjolnir looks for a token in GH_TOKEN, then GITHUB_TOKEN, then the authenticated GitHub CLI. The active token is injected into managed non-local sessions and kept out of checkpoints and recovery archives.

Local Podman, Docker, SSH Podman, SSH Docker, and Apple Container targets maintain a read-only Git object cache on the container host under ~/.cache/mjolnir/git. Mjolnir refreshes a bare mirror, makes an isolated per-session snapshot with hardlinked immutable objects, and lets the target clone borrow from it. If cache setup is unavailable, the launch falls back to a normal network clone.

The cache can contain objects from private repositories and is created with user-only permissions. Unused mirrors are pruned after 30 days and the mirror set has a 20 GiB least-recently-used soft cap. Session snapshots are removed with their containers.

A local bundle member is a controller-side path used to resolve its network fetch and push configuration. Mjolnir creates an independent target clone on the fetch remote’s advertised default branch. The target’s origin keeps the fetch URL and the configured push destination(s), including a separate push repository when one is set.

The source checkout is never mounted or copied into the target, and Mjolnir does not provide a Git service backed by it. Unpublished commits, staged changes, unstaged changes, and untracked files stay on the host. If the source has no usable network remote, isolated creation fails before provisioning; a raw local session is the only no-remote exception. Suspending an isolated session saves its checkpoint and does not publish a branch to the host checkout. New network-backed sessions can resume from their saved checkpoint, including work made after the clone.

Managed targets inherit a small allowlist of useful controller Git settings, including identity, pull/rebase behavior, conflict style, rerere, and pruning. Each new clone uses push.default=current so a normal push publishes its session branch to the configured push destination(s). Targets do not inherit arbitrary Git configuration or credential helpers.

The primary repository is the session cwd. Other bundle repositories are sent through ACP as additional workspace directories, so a capable harness can reason across the set without pretending the repositories are one Git tree. Each repository keeps its own .git, origin, session branch, dirty state, and archive material.

Muse Code and MiMo Code ACP support one workspace root only. Pair either with a single-repository bundle or one bare project directory, and do not add attached directories. The other six supported harnesses, DSH included, accept multi-root bundles.

Attached directories are not bundle members. They are per-session supplemental resources selected in the creation or resume wizard, do not acquire Git origins, and have target-specific copy-on-write or snapshot behavior. See Session lifecycle and Targets.

Mjolnir gives new sessions a small persistent knowledge store scoped to the project. It is shared across harnesses and sessions that resolve to the same project identity:

  • a GitHub repository is identified by lowercased owner and repository;
  • another network repository keeps its server’s case-sensitive path;
  • a repository without a network remote uses its canonical repository root, qualified by host for SSH directories;
  • raw and managed single-repository projects share repository memory; and
  • a multi-repository project includes its primary repository, members, and layout, independent of member order.

The workspace name is not part of that identity. Two workspaces using the same project therefore share project memory; two different bundles do not merely because their display names happen to match.

Each session gets a private project-memory directory in its staged harness profile. Claude Code uses its native project-memory integration. Codex, Kimi, and Grok receive the directory as a readable and writable workspace root and use their own file tools to edit it. Muse can access it directly because its sandbox is disabled. The startup context gives non-Claude agents its path and supplies the first 200 lines or 25 KiB of MEMORY.md, so keep that index short and link to focused documents there. The mj-memory MCP server provides only session-history and code-provenance tools.

For each non-Claude memory file, keep one fact in a Markdown document with frontmatter for name, description, and metadata.type. Add a one-line pointer to each file in MEMORY.md. Update an existing note instead of duplicating it, and delete notes that are wrong. Claude Code keeps its native memory conventions. See Harness limitations.

A session can start without any of this. mj new --no-project-memory (or "no_project_memory": true in the session-creation API) adds no startup context, stages no memory directory, turns off Claude Code’s auto-memory, and leaves out the mj-memory history tools. The session’s sub-agents inherit the choice, and its memory is neither read from nor synchronized with the project’s canonical copy. The choice is stored with the session and kept on resume and Move. Other sessions are unaffected.

For a multi-root bundle, bundle-wide material lives at the virtual root and repository-specific material may live below /roots/<repository-id>/. The startup context also tells the agent which repository ID maps to each workspace root.

Project-memory files are synchronized at checkpoints and have these limits:

  • 100 KiB per file;
  • 1 MiB per synchronized snapshot;
  • 1024 bytes per path.

Empty files, unsafe path components, hidden or reserved path segments, and symbolic-link traversal are not synchronized. Delete a file from the session replica to remove it from project memory at the next checkpoint.

Every session works on a private replica. At checkpoints, Mjolnir combines it with the canonical controller copy using a line-by-line three-way merge. Additions and deletions sync too. When two sessions change the same lines, an available utility model merges their edits; if none is available, the session being merged wins. Mjolnir does not create a conflicts folder.

The canonical copy lives below:

<MJ_DATA_DIR or platform data directory>/projects/<project-key>/memory/

Memory is background context, not authoritative project state. Agents are told to verify it against the working tree. Do not store credentials or other secrets there; it is deliberately available to every session for that project and syncs at checkpoints.

Use a workspace for “which sessions and drafts do I want in this view?” Use a bundle for “which repositories make up this project?” Use project memory for small durable facts the agent should carry between sessions. Use the repository itself for source code, design documents, and anything that belongs under version control.