Skip to content

Repository files navigation

drun (deterministic run)

The virtualization backend for agentic workflows

Drun is a platform that allows you to virtualize components of your host into an ephemeral runtime to serve as the agent's workspace with git-like primitives which allow the agent to explore trajectories in parallel and discard dead-ends without disrupting the host state.

Drun surfaces a runtime abstraction layer with reliability harnesses to guardrail the agent's behavior across a range of OS-level aspects:

  • Network domains (e.g. allowlisted domains)
  • Command execution (e.g. forbidden commands)
  • Access to filesystem paths (e.g. restrict filesystem access)
  • Resource limits (e.g. memory and duration caps)

Rather than granting your agent raw access to your host, Drun exposes and enforces a highly-customizable policy layer with deterministic knobs for you to place absolute limits that can't be breached by design.

drun architecture

Usage

The drun framework can be consumed in the following ways:

Installing

All journeys except for the Python SDK require the drun-mcp daemon installed and running in the host machine to operate. This is done once with:

curl -fsSL https://raw.githubusercontent.com/dmosc/drun/main/install.sh | bash

This installs and configures a few things (skips if not applicable):

  1. The drun-mcp binary under /usr/local/bin/drun-mcp.
  2. A global config at ~/.drun/config.toml with sensible defaults.
  3. drun-mcp as a persistent background daemon (launchd on macOS, systemd on Linux) so a single process serves all simultaneous sessions running on the host.
  4. The setup wizard, to install everything else — the chat CLI, Ollama, Tailscale — one step at a time.

install.sh only handles the binary and the daemon — it does not wire up any agent. Once it's done, point the binary at whichever bridge you use:

# Run from a project root — `claude`/`gemini` init are per-project scoped;
# `codex`/`hermes` init also register machine-wide, but still need a per-project
# run to drop their context file (AGENTS.md/HERMES.md) into this project.
drun-mcp claude init
drun-mcp gemini init
drun-mcp codex init
drun-mcp hermes init

drun-mcp bridges list shows every bridge drun currently supports (name, scope, and what it does). drun-mcp bridges deregister-all undoes every bridge that's currently registered in one call.

See Claude Code, Gemini CLI, Codex CLI, and Hermes for what each of these does.

Once installed, the following endpoints are available:

Endpoint Purpose
http://127.0.0.1:7273/sse MCP transport (SSE); used by Claude Code
http://127.0.0.1:7273/mcp MCP transport (streamable HTTP); used by the CLI
http://127.0.0.1:7274 Web interface to manage sessions
http://127.0.0.1:7275 Setup wizard

Setup wizard

install.sh finishes by launching drun-mcp setup in the background and opening http://127.0.0.1:7275 in your browser. It never runs anything on your machine itself — it's a checklist: every dependency lists the exact command to install it and, right below, the command that undoes it. Copy a command into your own terminal (the wizard's Open Terminal button opens one), run it, fill in any password or prompt it asks for, then hit Validate dependencies to re-check and flip that dependency's pill from "Not ready" to "Ready":

  • drun daemon — confirms the MCP and web UI ports are actually up; no commands here, install.sh already handled it.
  • Homebrew (macOS only) — the package manager the Ollama/Tailscale commands below install through.
  • Ollama — install, start the local server, and pull a default tool-calling model, for the standalone CLI.
  • drun chat CLI — installs drun-sandbox[chat] via pip; powers drun chat and the web UI's per-session Chat button.
  • Tailscale — install, start its background daemon, sign in, and expose the web UI over your tailnet — see Manage drun remotely. Starting the daemon needs sudo, which is exactly why this only ever shows you the command instead of running it.

Upgrading

Run the following commands to upgrade drun's MCP to the latest release:

The upgrade operation hard-reloads the daemon process, effectively dropping all in-memory objects, including ongoing sessions. Be sure to snapshot and close your sessions before updating.

# MCP binary
curl -fsSL https://raw.githubusercontent.com/dmosc/drun/main/update.sh | bash

# Update to a specific version
curl -fsSL https://raw.githubusercontent.com/dmosc/drun/main/update.sh | bash -s -- v0.3.16

update.sh also upgrades drun-sandbox[chat] to its latest version, best-effort, if it's already installed — skipped quietly otherwise.

Uninstalling

Run the following command to uninstall drun from your host:

curl -fsSL https://raw.githubusercontent.com/dmosc/drun/main/uninstall.sh | bash
  1. Stops the background daemon and removes the launchd agent (macOS) or systemd user service (Linux).
  2. Unlinks the MCP from any bridge it was wired to (e.g. Claude Code, Gemini CLI, Codex CLI, Hermes) — via drun-mcp bridges deregister-all, which knows every bridge drun supports without uninstall.sh having to name them.
  3. Removes the drun MCP binary from /usr/local/bin/drun-mcp.
  4. Removes .claude/settings.json and .gemini/settings.json from every project drun was initialized in, so native tools are restored automatically.
  5. Leaves ~/.drun/config.toml and any per-project context files (CLAUDE.md, GEMINI.md, AGENTS.md, HERMES.md) untouched; delete these manually if not needed.

Claude Code

Requirements

Per-project setup

From the root of any project you want drun to manage:

drun-mcp claude init

This does two things:

  1. Registers drun with Claude Code (claude mcp add --scope user --transport sse drun http://127.0.0.1:7273/sse) — a one-time, user-scope step; skipped if already registered. If the claude CLI isn't on PATH, it prints this command instead so you can run it yourself once Claude Code is installed.
  2. Creates two files in the current directory (appends if they already exist):
    • .claude/settings.json — restricts Claude to drun tools only for this workspace. Native file (Read, Edit, Write, NotebookEdit, Glob, Grep), shell (Bash, BashOutput, KillBash), network (WebFetch, WebSearch), and subagent delegation (Task) tools are all blocked, and drun's MCP tools are pre-allowed so Claude isn't prompted on every call.
    • CLAUDE.md — instructs Claude how to use drun.

The tool restriction is intentionally per-project; you wouldn't want native tools blocked globally across every workspace. Run drun-mcp claude init from any project root to opt that project into the drun sandbox — the registration step is idempotent, so re-running it across projects doesn't re-register with Claude Code each time.

Validate that the MCP is live:

claude mcp list

To undo the global registration (leaving any per-project .claude/settings.json and CLAUDE.md files in place — remove those by hand if you no longer want them):

drun-mcp claude deregister

Gemini CLI

Requirements

Per-project setup

From the root of any project you want drun to manage:

drun-mcp gemini init

This does two things:

  1. Registers drun with Gemini CLI (gemini mcp add --scope user --transport sse drun http://127.0.0.1:7273/sse) — a one-time, user-scope step; skipped if already registered. If the gemini CLI isn't on PATH, it prints this command instead so you can run it yourself once Gemini CLI is installed.
  2. Creates two files in the current directory (appends/skips if they already exist):
    • .gemini/settings.json — excludes drun-overlapping native tools (ShellTool, EditTool, WriteFileTool, ReadFileTool, GlobTool, GrepTool, ReadManyFilesTool, LSTool, WebFetchTool, WebSearchTool) via excludeTools, for this workspace only.
    • GEMINI.md - instructs Gemini how to use drun tools.

Same as Claude Code, the tool restriction is per-project and the registration step is idempotent — re-running drun-mcp gemini init across projects doesn't re-register with Gemini CLI each time.

To undo the global registration (leaving any per-project .gemini/settings.json and GEMINI.md files in place):

drun-mcp gemini deregister

Codex CLI

Requirements

Setup

Run this from the root of any project you want drun to manage:

drun-mcp codex init

This does three things, all directly editing ~/.codex/config.toml (a structural TOML merge via toml_edit, so any comments or formatting you already have there survive untouched):

  1. Creates AGENTS.md in the current directory (skipped if it already exists) — Codex's own auto-discovered context file, same role CLAUDE.md plays for Claude Code.

  2. Registers drun under [mcp_servers.drun], pointing at the daemon's streamable-HTTP endpoint:

    [mcp_servers.drun]
    url = "http://127.0.0.1:7273/mcp"
  3. Disables Codex's native shell tool (features.shell_tool = false) so Codex relies on drun's sandboxed session_bash instead of running commands directly on the host. Because config.toml isn't project-scoped, this applies to every Codex session on the machine, not just projects using drun.

No CLI availability check is needed here (unlike Claude Code, Gemini CLI, or Hermes) — this is a plain file edit, so it works whether or not the codex binary is on PATH yet.

Steps 2 and 3 are machine-wide and idempotent — re-running drun-mcp codex init in a second project skips them and only step 1 (AGENTS.md) does anything new.

To undo everything codex init did (deregisters drun and re-enables the shell tool; leaves any project's AGENTS.md in place):

drun-mcp codex deregister

Hermes

Requirements

Setup

Run this from the root of any project you want drun to manage — same as drun-mcp claude init for Claude Code:

drun-mcp hermes init

This does three things:

  1. Creates HERMES.md in the current directory which instructs Hermes how to rely no drun.

  2. Registers drun by writing a drun entry directly into ~/.hermes/config.yaml under mcp_servers, pointing at the daemon's streamable-HTTP endpoint:

    mcp_servers:
       drun:
          url: "http://127.0.0.1:7273/mcp"
          headers:
             Accept: "application/json, text/event-stream"

    If the hermes CLI isn't on PATH yet, it prints this block instead so you can add it manually once Hermes is set up.

  3. Disables Hermes's native terminal, file, web, search, and delegation toolsets (via agent.disabled_toolsets in the same file) so Hermes relies on drun's sandboxed tools instead of touching the host directly. Because this key isn't project-scoped, it applies to every Hermes session on the machine, not just projects using drun — if you want Hermes to keep native tool access for other work, skip this step and edit ~/.hermes/config.yaml by hand to register just the mcp_servers entry.

Steps 2 and 3 are machine-wide and idempotent — re-running drun-mcp hermes init in a second project skips them (already registered/already disabled) and only step 1 (HERMES.md) actually does anything new.

Start Hermes and it will discover drun's tools at connect time:

hermes chat

To undo everything hermes init did (deregisters drun and re-enables the disabled toolsets machine-wide; leaves any project's HERMES.md in place — same as Claude Code leaves CLAUDE.md, delete it by hand if you no longer want it):

drun-mcp hermes deregister

Standalone CLI

drun chat drives an LLM — local via Ollama or any cloud model supported by LiteLLM — against a sandboxed session.

Requirements

  • Python 3.9+.
  • The drun-mcp daemon installed above.
  • Ollama for local models, or an API key for a cloud model.

The setup wizard installs this with one click; check with command -v drun before running this by hand:

pip install 'drun-sandbox[chat]'

For a local model, install Ollama and pull a tool-calling-capable model. The setup wizard does both with one click each; by hand:

ollama pull qwen3.6:latest

Then run:

drun chat "your prompt" --mount ./my-project

--model defaults to ollama_chat/qwen3.6:latest. To use a cloud model instead, pass --model and set the provider's API key:

ANTHROPIC_API_KEY=... drun chat "your prompt" --model claude-sonnet-4-6

Each drun chat call creates a new session by default. Pass --session-id to attach to one that's already running instead:

drun chat "keep going on the report in results.md" --session-id <id>

Run drun chat --help for all flags.

Python SDK

Useful to spin up drun sessions programatically.

Requirements

  • Python 3.9+.
pip install drun-sandbox
from drun import Session

session = Session()
session.write_file("hello.py", b"print('hi')")
checkpoint = session.execute_bash("python3 hello.py")
print(checkpoint.stdout)

See examples/quickstart.py for a fuller walkthrough (bash execution, write, diff, rollback, export).

Configuration

The behavior of the drun MCP is orchestrated via ~/.drun/config.toml, a single global file shared by the background daemon. It's re-read on every tool call; without it, built-in defaults apply.

The following is a reference of all the controls available for tuning. All fields are optional.

Field Default Description
domain_allowlist ["pypi.org", "files.pythonhosted.org", "cdn.jsdelivr.net"] Domains reachable via session_fetch. Defaults to the three built-ins if the key is absent; setting it explicitly (including []) replaces the defaults outright, so an operator can restrict below them. Use ["*"] to allow all, or "*.example.com" for subdomains.
fetch_timeout_ms 60000 Timeout for the full session_fetch response in milliseconds.
connect_timeout_ms 30000 TCP connection timeout for session_fetch in milliseconds.
bash_timeout_ms 30000 Maximum wall time for a single session_bash call.
package_install_enabled false Enables session_package_install. Disabled by default because, unlike session_bash, its sandbox has network access (confined to a disposable staging directory, never the session workspace).
package_install_timeout_ms 180000 Maximum wall time for a single session_package_install call.
max_workspace_mb 512 Maximum workspace size per session in megabytes. Checked before each new checkpoint is appended.
max_sessions 50 Maximum number of concurrent sessions.
max_checkpoints 200 Maximum checkpoints stored per session. When the limit is reached, squash or drop old checkpoints.
session_idle_timeout_secs 3600 Seconds of inactivity before a session is considered abandoned and rejected.
mount_allowlist [] Host path prefixes that session_mount may read from, and session_export/delete_from_host may write to or delete. Empty means all paths are permitted.
mount_overlay_paths ["node_modules", ".venv", "venv", "target", "__pycache__", ".git"] Directory names that session_mount registers as read-only host overlays instead of loading into the workspace. Overlay dirs are symlinked at execution time and never checkpointed. Set to [] to disable.
snapshots_dir "~/.drun/snapshots" Directory where session_snapshot writes .drun files.
snapshot_on_close false When true, automatically write a snapshot when session_close is called.
env_allowlist [] Host environment variable names exposed to agents via session_get_env. Empty means no variables are exposed.
bash_command_denylist [] Command substrings always rejected by session_bash before execution.
bash_command_allowlist [] Command substrings permitted by session_bash. Empty means all commands are allowed (subject to the denylist).
web_port 7274 TCP port for the trajectory viewer web UI. Set to 0, or remove the field from the config file, to disable it.
default_model "ollama_chat/qwen3.6:latest" See LiteLLM's provider list for full list (e.g. claude-sonnet-4-6, gpt-4o, openrouter/anthropic/claude-sonnet-4-6)
anthropic_api_key unset API key exported as ANTHROPIC_API_KEY for Anthropic's backend.
openai_api_key unset API key exported as OPENAI_API_KEY for OpenAI's backend.
openrouter_api_key unset API key exported as OPENROUTER_API_KEY for OpenRouter's backend.
deepseek_api_key unset API key exported as DEEPSEEK_API_KEY for DeepSeek's backend.
gemini_api_key unset API key exported as GEMINI_API_KEY for Gemini's backend.

Updating configuration via the CLI

A couple of utility commands to update the configuration via the drun-mcp CLI are available:

drun-mcp config add-domain example.com
drun-mcp config add-path /path/to/allow
drun-mcp config remove-domain example.com
drun-mcp config remove-path /path/to/allow
# To validate latest changes to config.
drun-mcp config list

Run drun-mcp config --help to print a list of available commands.

~/.drun/config.toml is re-read on every tool call, so edits — via the CLI above or by hand — take effect on the very next call, no restart, no dropped sessions. drun-mcp claude init also allowlists the current project directory for session_mount automatically.

The exceptions are web_port, session_idle_timeout_secs: these are only applied at daemon startup, so changing any of them still requires a restart:

macOS

launchctl unload ~/Library/LaunchAgents/com.drun.mcp-server.plist
launchctl load -w ~/Library/LaunchAgents/com.drun.mcp-server.plist

Linux

systemctl --user restart drun-mcp.service

Verifying the daemon is healthy

A dead or crash-looping daemon can look identical to an idle one from the outside. See docs/troubleshooting.md's Health check section for commands to confirm it's running exactly once, actually listening, and not stuck being killed and retried by launchd/systemd.

Manage drun remotely

We recommend setting up Tailscale across your devices to remotely manage drun. The setup wizard installs Tailscale, walks you through signing in, and exposes the web UI in one click each — or by hand, roughly the steps are as follows:

  1. Install drun and make sure that the web service is up by visiting http://127.0.0.1:7274.

  2. Download Tailscale on the different devices you want to connect (e.g. computer and phone).

  3. Boot up a Tailscale server pointing at the drun webserver with:

    tailscale serve --bg --https=443 http://127.0.0.1:7274
  4. If the command succeeds, your web server UI should be accessible at the domain printed in the console by Tailscale.

To drive sessions from your phone via the web UI's Chat button — the main point of remote management, since it needs no terminal — the host also needs drun-sandbox[chat] installed. The setup wizard does this with one click; confirm with command -v drun on the host, or see Standalone CLI to install it by hand.

Chat from the web UI

Each session card has a Chat button — send it a prompt and the daemon runs drun chat against that exact session, so the agent gets the daemon's full sandboxed tool suite (session_bash, session_fetch, ...) under the same guardrails as any other bridge. Handy paired with remote management: drive a session from your phone without a terminal.

Requires the standalone CLI installed on the same machine as the daemon — one click in the setup wizard, or by hand:

pip install 'drun-sandbox[chat]'

The card's status pill shows "Running" while the agent works; refresh the session's checkpoint list to see what it did.

By default the agent runs against a local Ollama model. To use a cloud model instead (Claude, OpenAI, OpenRouter, DeepSeek, or Gemini), open Edit config in the web UI and set default_model plus the matching provider's API key — see Configuration for the field names. No restart needed; the next Chat request picks it up.

About

Deterministic runtime for AI agents.

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages