fffe/clod

clod

★ 0Forks 0ShellGitHub ↗Compare

README

clod

clod gives every Claude Code or Codex CLI session its own throwaway container.

It runs on rootless podman and is deliberately small: one script, one image, one credential volume. Both agents can live in the same image, or you can bake just one; the config you keep (MCP servers, skills, profiles) is written once and works for either.

This Doesn't Work On Ubuntu

Ubuntu's apparmor package ships with a policy for bubblewrap, presumably intended for system use (flatpak etc). Unfortunately it also picks up bwrap in the container, and breaks things (as of July 2026). Claude Code sandboxes with bubblewrap, and since 0.153 so does Codex (the older Landlock path sits behind use_legacy_landlock), so it bites both agents.

For my use case:

  • Bubblewrap is used to protect the agent from itself; system containment is provided by the container.
  • The Linux instances used are dedicated (no other users, no other tasks, no host bubblewrap).

If that's true for you, you can disable the policy like this:

apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict
ln -s /etc/apparmor.d/bwrap-userns-restrict /etc/apparmor.d/disable/

If it isn't, you're left with disabling the agent's sandbox (for Codex, set sandbox_mode = "danger-full-access" in preseed/codex/config.toml or switch it back to Landlock with [features] use_legacy_landlock = true) or choosing a distro that doesn't ship Apparmor.

Install

clod needs rootless podman with pasta networking, and jq (it renders the agents' config on the host). On Ubuntu 26.04:

apt install catatonit podman passt uidmap jq --no-install-recommends
systemctl stop netavark-firewalld-reload.service netavark-dhcp-proxy.socket netavark-dhcp-proxy.service
systemctl disable netavark-firewalld-reload.service netavark-dhcp-proxy.socket netavark-dhcp-proxy.service
killall -TERM netavark
loginctl enable-linger "$USER"

You don't need to stop the services, but they're not necessary.

Then put clod on your PATH and build the image:

chmod +x clod
ln -s "$PWD/clod" ~/.local/bin/clod     # or copy it anywhere on your PATH
clod build

You only need to add the symlink -- clod will locate the Containerfile, preseed/, and profiles/ next to the real script.

Usage

clod build [--no-claude] [--no-codex] [--profile NAME]...

Builds the clod:latest image from the Containerfile, with both Claude Code and Codex baked in. --no-claude or --no-codex leaves one out; at least one must stay. The image records which agents it carries, so clod run --codex on an image built with --no-codex fails up front instead of at launch.

Codex is installed from the release's codex-package asset, so ~/.local/bin gets both codex and the codex-code-mode-host binary that Code Mode -- on by default since 0.153 -- spawns beside it. The bare codex asset ships without that host, and Code Mode then fails on every turn that uses it.

Extra arguments pass through to podman build, so clod build --no-cache works. With --profile NAME, it bakes that profile's packages into a per-profile image instead (see Profile packages).

clod login [--claude|--codex]

Opens a one-off box, starts the agent's login, and copies the resulting token into the persistent clod-auth volume, so every box afterward starts already authenticated. Pick the agent with --claude or --codex; if the image only carries one, it's picked for you.

  • Claude: run /login and complete it. If the browser callback fails, paste the code at the prompt.
  • Codex: a device-auth flow; complete it in a browser on the host. If device auth isn't an option, export OPENAI_API_KEY on the host instead -- clod run passes it through and no login is needed.

You should only need to do this once per agent -- repeat it only if the token expires. Both tokens share the one volume.

clod run [-n NAME] [--profile NAME]... [--skill DIR]... [--mcp FILE]... [--claude|--codex|--shell] [DIR]

Starts a box with your project mounted at /workspace and drops you into it.

  • DIR -- the directory to mount. Defaults to the current directory. Only one is allowed.
  • -n, --name NAME -- name the box. Defaults to the workspace folder's name, with a suffix added if that name is already taken.
  • --profile NAME -- overlay profiles/NAME/ on top of the base config. Repeatable; when profiles conflict, the later one wins.
  • --skill DIR -- add a skill directory to this box only (both agents see it). Repeatable.
  • --mcp FILE -- add an MCP server definition (a JSON file, see MCP servers) to this box only. Repeatable.
  • --shell -- open a shell in the box. This is the default.
  • --claude / --codex -- launch straight into that agent instead of a shell.

The box runs detached, so you can attach more shells to it. Tokens are restored from clod-auth on start, and ANTHROPIC_API_KEY / OPENAI_API_KEY are passed through if set in your environment (each only when its agent is in the image). Set CLOD_MEMORY or CLOD_CPUS to cap the box's resources.

Everything derived -- merged MCP servers, the workspace trust marker, Codex's TOML -- is rendered on the host at launch and handed to the box as read-only files; the entrypoint copies them into the fresh agent homes and wires in ripwire. While the box runs, a small inotify watcher mirrors any mid-session token refresh straight back into clod-auth, so a refreshed token survives even if the box exits abruptly (no graceful clod stop). When several boxes share the volume, the freshest token always wins, so they never clobber each other.

clod attach NAME [-- CMD...]

Opens another shell in a running box -- handy for a second pane next to your clod run session. With -- CMD..., it runs that command in the box instead of opening a shell.

clod ls

Lists your running clod boxes and how long each has been up.

clod stop NAME | --all

Stops the named box, or every clod box with --all.

Before stopping, clod flushes the box's current tokens back into clod-auth (a final pass on top of the live inotify sync), so a token that refreshed mid-session isn't lost.

clod update [-y]

Checks for the latest Claude Code, Codex, and ripwire releases and, for each that is newer than the version pinned in the Containerfile, rewrites the pinned version and the per-arch sha256 sums to match. Claude's checksums come from the download host's manifest for that exact version; Codex's and ripwire's are the GitHub release assets' digests. Either way they match the bytes the build will fetch.

It shows the changes and asks before touching anything; -y skips the prompt. Each patch is anchored to the exact version and platform lines and verified after the fact, so a malformed Containerfile makes it refuse rather than edit the wrong line. Rebuild afterwards with clod build --no-cache.

Needs jq, git, and curl (or wget) on the host. Override the sources with CLOD_CLAUDE_GH_REPO, CLOD_CLAUDE_DL_BASE, CLOD_CODEX_GH_REPO, CLOD_RIPWIRE_GH_REPO, and CLOD_GH_API if you mirror the release feeds.

clod pins [--profile NAME]... [-y]

Refreshes the pinned versions in profiles' packages.mise and packages.git files. For each pin it resolves the latest version (mise latest for mise specs; the newest GitHub tag for git specs), prints current -> latest with a best-effort diff link (a GitHub compare URL where the source is on GitHub, else an npm/pkg.go.dev versions page), asks, then rewrites the files. -y skips the prompt; --profile limits the scope.

Refreshes stay within the current major (a 6.0.3 pin tracks the latest 6.x, never 7.x) so a bump can't drag you across a breaking major; to move majors, edit the pin's major by hand and refresh from there.

Each bump is an exact-line rewrite that must match exactly one line, so it can't touch the wrong pin. Rebuild the affected images afterwards with clod build --profile <name>. Version resolution uses mise on the host if present, otherwise the clod:latest image; git tags need git.

Customization

Preseed

preseed/ is your base config for both agents.

It is mounted read-only and copied into a fresh, throwaway agent home on every run -- so it stays version-controlled, never drifts, and never leaks between sessions. Edit it and the change applies on the next clod run; no rebuild needed.

  • preseed/claude/claude.json -- the base ~/.claude.json
  • preseed/claude/config/ -- copied over ~/.claude/ (settings.json, keybindings.json, plugins/, agents/, commands/)
  • preseed/codex/config.toml -- the base ~/.codex/config.toml
  • preseed/codex/config/ -- copied over ~/.codex/ (agents/, skills/, AGENTS.md). Not config.toml: that one is rendered and installed after this copy, so a copy of it here would just be overwritten.
  • preseed/mcp/*.json -- MCP servers for every box, both agents (ships ripwire.json, see ripwire)
  • preseed/skills/*/ -- skills for every box, both agents

Point CLOD_PRESEED at another directory to use a different base.

~/.codex/config.toml is a single TOML file, so it's built by concatenation: the base config first, then profile fragments, then a trust table for the workspace, then the MCP tables. Everything after the base only adds [tables], so a profile fragment must not redefine a top-level scalar (model, sandbox_mode, ...) already set in the base.

MCP servers

MCP servers are written once, in Claude Code's JSON schema, and serve both agents. Every mcp/*.json (preseed, then profiles, then --mcp; later wins) is merged into ~/.claude.json for Claude and rendered as [mcp_servers.NAME] tables in ~/.codex/config.toml for Codex. The mapping:

  • stdio: command, args, env, cwd -- the same keys on both sides.
  • HTTP: "type": "http", url, headers -- url and http_headers for Codex. sse and ws servers have no Codex transport and are skipped for Codex, with a warning at launch.
  • ${VAR} and ${VAR:-default} are expanded from the host environment at launch, for both agents, so one file behaves the same in either. An unset variable with no default is left as written.
  • Per-agent extras: a "claude": {...} block inside a server entry is merged into that entry for Claude only; a "codex": {...} block is emitted as extra TOML keys for Codex only (enabled_tools, bearer_token_env_var, env_http_headers, ...). Neither agent sees the other's block.

See preseed/mcp/README.md for examples and preseed/mcp/ripwire.json for a per-agent block in use.

Skills

Skills (directories with a SKILL.md) are agent-agnostic. Everything under preseed/skills/, a profile's skills/, or --skill DIR is copied into ~/.claude/skills/ for Claude and ~/.agents/skills/ (Codex's user-level skills root) for Codex, so one set serves both. ripwire's bundled skills are linked in beside them by ripwire's own installer (see ripwire).

Subagents and commands

Unlike skills and MCP servers, these have no shared format -- each agent gets its own file, in its own config overlay. The preseed ships one set of four subagents (thinker, developer, reviewer, bulk-ops) and two workflows (plan-and-build, optimize) written twice, once per agent:

Claude Codex
subagents preseed/claude/config/agents/NAME.md (YAML frontmatter) preseed/codex/config/agents/NAME.toml
workflows preseed/claude/config/commands/NAME.md -- a /NAME slash command preseed/codex/config/skills/NAME/SKILL.md -- Codex has no commands dir

Codex only discovers a skill as a directory with a SKILL.md; a bare skills/NAME.md is silently ignored. To keep a Codex skill explicit-only (the equivalent of Claude's disable-model-invocation: true), add skills/NAME/agents/openai.yaml with policy.allow_implicit_invocation: false -- both shipped workflows do.

Keep the two sides in sync by hand, or delete the half you don't use.

Profiles

A profile is an optional overlay you pull in per box with --profile NAME (repeatable and composable). Each profiles/NAME/ may contain:

  • mcp/*.json -- extra MCP servers, both agents.
  • skills/*/ -- extra skills, both agents.
  • claude/config/ -- files copied over ~/.claude/ after the preseed (later profiles overwrite earlier ones).
  • codex/config.toml -- appended onto ~/.codex/config.toml after the preseed (later profiles win; add [tables] only).
  • codex/config/ -- files copied over ~/.codex/ after the preseed (later profiles overwrite earlier ones).
  • packages.mise -- mise tools to bake in at build time (see below).
  • packages.git -- git-based npm packages to bake in (things not on the npm registry, e.g. a fork), pinned by tag.
  • packages.apt -- system packages installed as root at build time.

The included profiles/personal/ adds a GitHub MCP server: put your token in profiles/personal/mcp/github.json (or delete it), then run clod run --profile personal.

Point CLOD_PROFILES at another directory to keep profiles elsewhere.

Profile packages

mcp/, skills/, claude/, and codex/ are just files, so they're overlaid onto a fresh agent home on every run. Tools are different -- they have to be installed -- so a profile lists them in packages.mise (one mise tool spec per line) and they're baked into the image at build time:

clod build --profile go            # builds clod:go with the go tools baked in
clod run   --profile go ~/code     # uses clod:go

clod run --profile go picks the matching image automatically and tells you to build it if it's missing. It also checks the image's clod.profiles label, so asking for a profile whose packages were never baked -- possible when CLOD_IMAGE pins the image and the tag can't say -- is an error rather than a box that looks right and has none of the tools. Each package profile installs into its own directory, placed on PATH ahead of the base toolchain but below your ~/.local/bin -- so you can always shadow a profile's tool with your own, and when several package profiles are combined the later --profile wins. Profiles that carry only mcp/skills/claude/codex need no build and keep running on the base clod:latest.

Three manifests, each baked at clod build --profile <name>:

  • packages.mise -- one mise spec per line (go:..., npm:..., aqua:...), installed into the profile's own isolated mise dir.
  • packages.git -- one <git-url>#<tag> per line, installed with npm install -g into the profile's own npm prefix. For npm packages that aren't on the registry (mise's npm backend can't resolve git refs).
  • packages.apt -- one apt package per line, installed system-wide as root. apt can't be scoped to a per-profile dir, so keep these to genuine system deps. Baking runs the build as root for these; browsers/services are not started.

packages.mise and packages.git entries are pinned and refreshed by clod pins. Baking reads profiles from the build context, so they must be in the default profiles/ next to the script (a relocated CLOD_PROFILES can't be baked).

The shipped package profiles:

  • go -- the pinned go toolchain (newer than Ubuntu ships) plus gofumpt and dlv. The base image has no go, so reach for this profile whenever you're writing Go.
  • bash -- a pinned shellcheck, newer than the base apt one.
  • js -- typescript, eslint, typescript-eslint, esbuild, vitest, playwright. Its packages.apt carries Playwright's Ubuntu 26.04 browser dependencies (chromium/firefox/webkit + the tools set); the browser binaries themselves are downloaded by Playwright on first use, not baked.

ripwire

Code intelligence comes from ripwire, a single-binary code-context tool (symbol maps, callers, blast radius, tests to run) that needs no language servers and works the same for either agent. The pinned release is baked into every image and refreshed by clod update.

Every box is wired the way ripwire wrap claude and ripwire wrap codex print, step for step, so ripwire /workspace --doctor --agent=claude (or --agent=codex) passes in a fresh box:

  • MCP server -- preseed/mcp/ripwire.json registers ripwire --mcp for both agents. Claude gets all 31 verbs. Codex, per ripwire's own recipe, gets the four audit/health verbs (analyze, quality_delta, flags, doc_drift) with default_tools_approval_mode = "approve". Delete the codex block to expose everything to Codex, or delete the file to drop the server altogether; the CLI, skills and hooks below stay either way.
  • Skills -- the entrypoint runs ripwire's own skills/install.sh for each agent. It symlinks the bundled skills into ~/.claude/skills/ and ~/.agents/skills/ and writes the manifest the doctor audits.
  • Hooks -- the same installer run (--hook) registers the advisory PreToolUse meter, SessionStart primer and UserPromptSubmit router in ~/.claude/settings.json and ~/.codex/hooks.json. They never block a call. Claude runs its copy as is. Codex runs a user-level hook only after a /hooks review, and it records that review in ~/.codex/config.toml as a hash of each hook's definition -- which a box throws away. So the image computes exactly those records at build time (the Containerfile carries Codex's hashing rules, checked against 0.154.0) and the entrypoint appends them to the box's config.toml. /hooks inside Codex then lists the three ripwire hooks as trusted, and project or plugin hooks keep their normal review.
  • Instructions -- the use-when blurb wrap prints is appended to ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md, so every project gets it without a per-repo paste. A CLAUDE.md shipped in preseed/claude/config/ keeps its content; the blurb goes below it.

The hooks' substitution meter writes ~/.ripwire/*.jsonl inside the box and is discarded with it; RIPWIRE_METER=0 in the box turns the counting off.

Contributors

fffe

Issues