md

package module
v0.6.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

README

md: Branch-locked Development Containers for AI Coding Agents

Each container is locked to a repository-branch pair. No confusion. Safe parallel work.

Safe parallel work with multiple AI coding agents. Run Claude Code, Codex, Amp CLI, Gemini CLI, Kilo CLI, Pi, and other tools in isolated containers without branch conflicts, file interference, or environmental headaches.

The Problem

AI coding agents work best when given full command execution (YOLO mode). But running them locally is risky:

  • Branch conflicts - Agent changes on one branch interfere with your local checkout
  • Test conflicts - Running tests simultaneously causes failures and race conditions
  • Environment pollution - Dependencies and state accumulate, causing hidden bugs
  • Context switching - Switching branches while an agent is working loses progress

The Solution

md gives each AI agent a complete, isolated container with a full git clone. You can:

  • Run agents on multiple branches simultaneously
  • Switch local branches without affecting running agents
  • Run tests in parallel without conflicts
  • Keep your local checkout clean
  • Delete containers cleanly when done
  • Access your frontend dev server over Tailscale with HTTPS!
  • Share your host's USB port for Android debugging
  • Let the coding agent control an Android Emulator and see it over VNC

Quick Start

# Start container for your current branch; this automatically ssh in.
git checkout -b wip origin/main
md start

# You are now inside the container
cd ~/src/<repo-name>
# Run the coding harness of your choice, a bash alias will automatically start it in YOLO mode:
claude
# Exit from the ssh session into the container (or use a separate terminal)
exit

# Check pending changes
md diff

# Pull changes back when done
md pull

# Clean up the container
md kill

Installation

go install github.com/maruel/md/cmd/md@latest

Recommended: Also install git-maruel for the git squash and git rb helpers.

Harnesses

For coding agents: The container includes /home/user/src/AGENTS.md which provides information about preinstalled tools (list available in ~/src/tool_versions.md).

Harnesses preinstalled:

  • amp: ~/.config/amp/AGENTS.md
  • claude: ~/.claude/CLAUDE.md
  • codex: ~/.codex/AGENTS.md
  • gemini: ~/.gemini/GEMINI.md
    • Recommended in ~/.qwen/settings.json to change "context" / "fileName" to AGENTS.md
  • kilo: ~/.kilocode/rules/*.md
  • opencode: ~/.config/opencode/AGENTS.md
  • pi: ~/.pi/agent/AGENTS.md
  • qwen: ~/.qwen/QWEN.md
    • Recommended in ~/.qwen/settings.json to change "context" / "fileName" to AGENTS.md
Readme for agents (https://agents.md) and Skills (https://agentskills.io)

FYI, here's locations of AGENTS.md for each harness:

mkdir -p ~/.config/agents ~/.config/amp ~/.claude ~/.codex ~/.gemini ~/.kilocode/rules ~/.config/opencode ~/.pi/agent ~/.qwen
echo "Read ~/AGENTS.md if present." >> ~/.config/agents/AGENTS.md
ln -s ../../.config/agents/AGENTS.md ~/.config/amp/AGENTS.md
ln -s ../.config/agents/AGENTS.md ~/.claude/CLAUDE.md
ln -s ../.config/agents/AGENTS.md ~/.codex/AGENTS.md
ln -s ../.config/agents/AGENTS.md ~/.gemini/AGENTS.md
ln -s ../../.config/agents/AGENTS.md ~/.kilocode/rules/AGENTS.md
ln -s ../../.config/agents/AGENTS.md ~/.config/opencode/AGENTS.md
ln -s ../../.config/agents/AGENTS.md ~/.pi/agent/AGENTS.md
ln -s ../.config/agents/AGENTS.md ~/.qwen/AGENTS.md

FYI, here's locations of skills for each harness:

  • amp: ~/.config/agents/skills/**/SKILL.md (recursive)
    • Fallbacks to ~/.claude/skills/
  • antigravity: ~/.gemini/antigravity/skills/<name>/SKILL.md
  • claude: ~/.claude/skills/<name>/SKILL.md
  • codex: ~/.codex/skills/**/SKILL.md (recursive)
  • cursor: ~/.cursor/skills/<name>/SKILL.md
    • Fallbacks to ~/.claude/skills/
  • gemini: ~/.gemini/skills/<name>/SKILL.md
  • kilo: ~/.kilocode/skills/<name>/SKILL.md
  • opencode: ~/.config/opencode/skill/<name>/SKILL.md
    • Fallbacks to ~/.claude/skills/
  • pi: ~/.pi/agent/skills/**/SKILL.md (recursive)
    • Fallbacks to ~/.claude/skills/, ~/.codex/skills/ (recursive)
  • qwen: ~/.qwen/skills/<name>/SKILL.md

Centralize your skills with symlinks:

mkdir -p ~/.config/agents/skills ~/.claude ~/.codex ~/.cursor ~/.gemini/antigravity ~/.kilocode ~/.config/opencode ~/.pi/agent ~/.qwen
ln -s ../.config/agents/skills/ ~/.claude/skills
ln -s ../.config/agents/skills/ ~/.codex/skills
ln -s ../.config/agents/skills/ ~/.cursor/skills
ln -s ../../.config/agents/skills/ ~/.gemini/antigravity/skills
ln -s ../.config/agents/skills/ ~/.gemini/skills
ln -s ../.config/agents/skills/ ~/.kilocode/skills
ln -s ../../.config/agents/skills/ ~/.config/opencode/skill
ln -s ../../.config/agents/skills/ ~/.pi/agent/skills
ln -s ../.config/agents/skills/ ~/.qwen/skills

How It Works

Container Setup

Each container is named md-<repo-name>-<branch-name> with:

  • Isolated git clone - ~/src/<repo-name> inside the container is a git clone of your local repository. It tracks branch base which matches your local branch. This is useful for commit-happy agents like Codex to track pending changes.
  • User-mapped permissions - Container runs as your local user ID for proper file permissions
  • SSH access - Connect via ssh md-<repo>-<branch>
  • Remote GUI (VNC) - Optional full desktop environment (via -display) accessible via VNC on a dynamic port
  • Remote network (Tailscale) - Optional full access to your tailnet (via -tailscale)
  • Local USB debugging - Optional USB debugging (via -usb), especially for Android development
  • Minimal overhead - Only sshd runs by default; no unnecessary background services
Preinstalled Tools
Configuration

Agent configurations and credentials are automatically mounted:

  • Agent configurations: ~/.amp, ~/.claude, ~/.codex, ~/.gemini, ~/.kilocode, ~/.kimi, ~/.pi, ~/.qwen, ~/.config/agents, ~/.config/amp, ~/.config/goose, ~/.config/opencode, ~/.local/share/amp, ~/.local/share/goose, ~/.local/share/opencode, ~/.local/state/opencode
  • Android ADB keys: ~/.android
  • Gradle cache: ~/.gradle/caches and ~/.gradle/wrapper/dists

Environment variables can be passed via:

  1. .env file in your repository (auto-mapped)
  2. ~/.config/md/env on your local machine (applies to all containers)

For example:

# ~/.config/md/env
ANTHROPIC_API_KEY=your_key
OPENAI_API_KEY=your_key
GitHub Authentication

The container doesn't have access to your GitHub credentials. To enable git credentials and access to GitHub (e.g. to create PRs or issues), authenticate inside the container via:

gh auth login

Commands

Command Purpose
md start Create and start a container for the current branch
md start -display Start container with X11/VNC desktop environment enabled
md start -tailscale Start container with Tailscale networking for remote SSH access
md start -usb Start container with USB device passthrough (for ADB, etc.)
md run <cmd> Start a temporary container, run a command, then clean up
md list List all md containers
ssh md-<repo>-<branch> Access the container via SSH
md vnc Open VNC connection to the container
md diff Show changes (runs git diff base). Arguments are passed through, e.g. md diff -- --stat
md pull Pull changes from container back to local branch
md push Push local changes to the container
md kill Stop and remove the container
md build-image Build the base Docker image locally as md-local
md start -image md-local Start a container using the locally built base image

Remote GUI Access (VNC)

Each container can include a full XFCE4 desktop environment. It must be enabled at startup:

md start --display
md vnc

md vnc opens the VNC connection in your default VNC client.

Recommended VNC clients by OS:

The DISPLAY environment variable is automatically set in SSH sessions, so X11 applications launched from SSH will appear on the VNC desktop.

Contributing

Made with ❤️ by Marc-Antoine Ruel. Contributions are very appreciated! Thanks in advance! 🙏

Documentation

Overview

Package md manages isolated Docker development containers for AI coding agents.

It provides programmatic access to create, manage, and tear down containers that each get a full git clone of your repository with SSH access.

Index

Constants

View Source
const DefaultBaseImage = "ghcr.io/maruel/md"

DefaultBaseImage is the base image used when none is specified.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// W is the writer for progress and status messages.
	W io.Writer

	// Paths.
	Home          string
	XDGConfigHome string
	XDGDataHome   string
	XDGStateHome  string

	// SSH key paths.
	HostKeyPath string // ~/.config/md/ssh_host_ed25519_key (generated)
	UserKeyPath string // ~/.ssh/md

	// Docker.
	ImageName string

	// Tokens.
	GithubToken string // GitHub API token for Docker build secrets.
	// TailscaleAPIKey is the Tailscale API key for auth key generation and device deletion.
	//
	// It is necessary to setup ephemeral nodes. The key must be rotated every 90 days.
	//
	// See https://tailscale.com/docs/reference/tailscale-api and
	// https://tailscale.com/docs/features/ephemeral-nodes
	TailscaleAPIKey string
	// contains filtered or unexported fields
}

Client holds global MD tool state (paths, image config, SSH keys).

func New

func New() (*Client, error)

New creates a Client with global MD tool config.

func (*Client) BuildImage added in v0.6.0

func (c *Client) BuildImage(ctx context.Context, serialSetup bool) (retErr error)

BuildImage builds the base Docker image locally.

func (*Client) Container

func (c *Client) Container(gitRoot, branch string) *Container

Container returns a Container handle for the given git root and branch.

It doesn't start it, it is just a reference.

func (*Client) List

func (c *Client) List(ctx context.Context) ([]*Container, error)

List returns running md containers sorted by name.

func (*Client) Prepare

func (c *Client) Prepare() error

Prepare ensures all directories and keys exist.

type Container

type Container struct {
	*Client
	// GitRoot is the absolute path to the git repository root on the host.
	// Label: md.git_root
	GitRoot string
	// RepoName is the basename of the repository directory.
	// Label: md.repo_name
	RepoName string
	// Branch is the git branch checked out in the container.
	// Label: md.branch
	Branch string
	// Name is the Docker container name (e.g. "md-myrepo-main").
	Name string
	// State is the Docker container state (e.g. "running", "exited").
	State string
	// CreatedAt is when the container was created.
	CreatedAt time.Time
	// Display indicates the container was started with X11/VNC enabled.
	// Label: md.display
	Display bool
	// Tailscale indicates the container was started with Tailscale networking.
	// Label: md.tailscale
	Tailscale bool
	// USB indicates the container was started with USB passthrough.
	// Label: md.usb
	USB bool

	// DefaultRemote is the host's default git remote (resolved lazily).
	DefaultRemote string
	// DefaultBranch is the default branch for DefaultRemote (resolved lazily).
	DefaultBranch string
}

Container holds state for a single container instance.

Fields marked with a label are persisted as Docker container labels and restored by [unmarshalContainer] when listing containers.

func (*Container) Diff

func (c *Container) Diff(ctx context.Context, stdout, stderr io.Writer, extraArgs []string) error

Diff writes the diff between base and current in the container. When stdout is a terminal, a TTY is allocated so git's pager and colors work.

func (*Container) Fetch

func (c *Container) Fetch(ctx context.Context, provider, model string) error

Fetch commits any uncommitted changes in the container and fetches them locally, updating the remote-tracking ref without integrating.

provider and model control AI commit message generation. See https://github.com/maruel/genai for valid names. If provider is empty, a default message is used.

func (*Container) GetHostPort

func (c *Container) GetHostPort(ctx context.Context, containerPort string) (string, error)

GetHostPort returns the host port mapped to a container port (e.g. "5901/tcp"). Returns empty string if the port is not mapped.

func (*Container) Kill

func (c *Container) Kill(ctx context.Context) error

Kill stops and removes the container.

func (*Container) Pull

func (c *Container) Pull(ctx context.Context, provider, model string) error

Pull fetches changes from the container and integrates them into the local branch.

provider and model control AI commit message generation. See https://github.com/maruel/genai for valid names. If provider is empty, a default message is used.

func (*Container) Push

func (c *Container) Push(ctx context.Context) error

Push force-pushes local state into the container.

func (*Container) Run

func (c *Container) Run(ctx context.Context, baseImage string, command []string) (_ int, retErr error)

Run starts a temporary container, runs a command, then cleans up. baseImage is the full Docker image reference; if empty, DefaultBaseImage is used.

func (*Container) Start

func (c *Container) Start(ctx context.Context, opts *StartOpts) (_ *StartResult, retErr error)

Start creates and starts a container.

func (*Container) SyncDefaultBranch added in v0.6.2

func (c *Container) SyncDefaultBranch(ctx context.Context) error

SyncDefaultBranch force-pushes the host's default branch (e.g. origin/main) into the container so agents can diff against it.

func (*Container) TailscaleFQDN added in v0.6.0

func (c *Container) TailscaleFQDN(ctx context.Context) string

TailscaleFQDN returns the Tailscale FQDN for the container, or "" if unavailable.

type StartOpts

type StartOpts struct {
	// BaseImage is the full Docker image reference (e.g.
	// "ghcr.io/maruel/md:v1.0" or "myregistry/custom:tag"). When empty,
	// DefaultBaseImage is used.
	BaseImage string
	// Display enables X11/VNC virtual display (port 5901).
	Display bool
	// Tailscale enables Tailscale networking inside the container.
	//
	// It is recommended to set Client.TailscaleAPIKey to enable ephemeral nodes. If Client.TailscaleAPIKey is
	// not set, the node will not be ephemeral. Instead, an authentication URL will be printed back by md.
	Tailscale bool
	// TailscaleAuthKey is a pre-authorized Tailscale auth key.
	//
	// When empty and Tailscale is true, Client.TailscaleAPIKey is used to generate an authentication key.
	//
	// The tailnet policy must allow "tag:md".
	//
	// https://tailscale.com/docs/features/access-control/auth-keys
	TailscaleAuthKey string
	// USB enables USB device passthrough (Linux only).
	USB bool
	// Labels are additional Docker labels (key=value) applied to the container.
	Labels []string
	// Quiet suppresses informational output during startup.
	Quiet bool
}

StartOpts configures container startup.

type StartResult added in v0.6.0

type StartResult struct {
	// SSHPort is the host port mapped to the container's SSH port.
	SSHPort string
	// VNCPort is the host port mapped to the container's VNC port, if display is enabled.
	VNCPort string
	// TailscaleFQDN is the Tailscale FQDN assigned to the container, if any.
	TailscaleFQDN string
	// TailscaleAuthURL is the Tailscale auth URL when no pre-auth key was provided.
	TailscaleAuthURL string
}

StartResult contains information about the started container.

Directories

Path Synopsis
cmd
md command
md (my devenv) manages isolated Docker development containers for AI coding agents.
md (my devenv) manages isolated Docker development containers for AI coding agents.
Package gitutil provides git utility functions for repository introspection, branch management, and pushing.
Package gitutil provides git utility functions for repository introspection, branch management, and pushing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL