md

package module
v0.19.2 Latest Latest
Warning

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

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

README

md: My Development containers

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, Kilo CLI, Pi, and other tools in isolated containers without branch conflicts, file interference, or environmental headaches.

codecov

Installation

curl caic.xyz/install.sh | bash
From source
go install github.com/caic-xyz/md/cmd/md@latest

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

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>
claude
exit

# Check what the container did that is not integrated on the host
md diff

# Check the whole branch
md diff -full

# Pull changes back when done
md pull
Git synchronization commands

md keeps a container checkout separate from the host checkout. Each command names the direction it moves work:

Command Effect
md start Copies the mapped branches, cached remote refs and tags into a new container, then checks out the primary branch there. Your branches do not move.
md diff Reports container work not yet integrated into the host branch. md diff -full reports the whole branch. It moves no branch.
md pull Commits the container's pending changes, then integrates every mapped branch into your host branches. It can rewrite host commit IDs.
md push Saves the container's Git-visible work on timestamped backup branches, then replaces the container's mapped branches with your host state.
md fork Snapshots the container and starts a new one on new host branches. The source container is untouched.
AI commit messages

md pull uses an available LLM provider to describe pending container changes before committing them. Set ASK_PROVIDER and ASK_MODEL to select a provider and model, or let md discover one automatically. Set the model context window with GIT_DESC_TOKENS or override it for one pull with --tokens. Pass -n or --no-describe to use a fixed commit message without contacting a provider. Pass -no-verify to skip the repository's pre-commit and commit-msg hooks while committing the container's pending changes. See the AI commit message documentation for provider configuration and large-diff behavior.

Every mapped host branch must exist and have an upstream, because the container branch tracks the same upstream. md reports an error and the repair command when that is not the case.

Integration points

When a command creates, replaces, or pulls a mapped branch, md remembers the container commit integrated into the host branch. md diff shows work after that point, while md diff -full shows the whole branch. Fetching a container branch for observation or durability does not move the integration point. The record survives the agent amending, resetting or rebasing its branch.

docs/GIT_MODEL.md explains what each side holds, what each command moves, and the cases where a diff reports something other than the work not yet integrated into the host branch.

Remote branches and fork workflows

md mirrors every cached remote branch from the host into the container. A task without network or repository credentials can therefore run commands such as git rebase origin/main or git rebase upstream/release. The refs are only as fresh as the host checkout; fetch on the host before starting the task when the latest remote state is required.

All host remotes are configured in the container. A branch's effective Git push remote is preserved independently from its upstream, so triangular workflows can rebase against upstream and push to origin. An actual network push from the container still requires repository access, such as md start --github for GitHub repositories.

md start maps all host tags by default, equivalent to --tags '.*'. Use md start --tags '<regexp>' to limit mapped tags, for example --tags '^v2\.', or --tags '' to map none. Quote the expression so the shell does not expand it. The same expression filters tags in initialized submodules. Selecting fewer tags avoids transferring old histories reachable only from tags in repositories with large tag sets. md run and the Go API retain opt-in semantics: an empty tag expression maps no tags.

Multiple mapped branches

md start --extra-branch <branch> maps additional branches into the same container. The first branch, your current branch or -b, remains the primary one and is the branch the container checks out. md diff reports whichever branch is checked out there; push, pull and fork cover every mapped branch.

md pull integrates mapped branches in order. If a later branch cannot be rebased, branches already completed remain integrated; the failed rebase is aborted and the original host checkout is restored.

Documentation

🔥 Full documentation is at docs.caic.xyz 🔥

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 with SSH access. Containers optionally receive a full git clone of one or more repositories; repo-less containers are also supported for general agent workloads.

Index

Examples

Constants

View Source
const DefaultBaseImage = "ghcr.io/caic-xyz/md-user"

DefaultBaseImage is the base image used when none is specified.

View Source
const DefaultMaxCPUs = -1

DefaultMaxCPUs selects an automatic CPU limit at container launch: leave two CPUs available to the runtime, allowing at least two CPUs when available. The runtime's CPU count may differ from the host's with Docker Desktop or a remote daemon. Zero still means no limit in StartOpts and ForkOpts.

Variables

View Source
var ErrDiffFound = errors.New("differences found")

ErrDiffFound is returned by Container.Diff when its Git arguments request an exit status for differences and at least one difference is found.

View Source
var HarnessMounts = map[Harness]AgentPaths{
	HarnessAmp:         {Description: "Amp", HomePaths: []string{".amp"}, XDGConfigPaths: []string{"amp"}, LocalSharePaths: []string{"amp"}},
	HarnessAndroid:     {Description: "Android Studio", HomePaths: []string{".android"}},
	HarnessAntigravity: {Description: "Antigravity", HomePaths: []string{".gemini"}},
	HarnessClaude:      {Description: "Claude Code", HomePaths: []string{".claude"}},
	HarnessCodex:       {Description: "Codex", HomePaths: []string{".codex"}},
	HarnessGoose:       {Description: "Goose", XDGConfigPaths: []string{"goose"}, LocalSharePaths: []string{"goose"}},
	HarnessKilo:        {Description: "Kilo Code", HomePaths: []string{".kilocode"}},
	HarnessKimi:        {Description: "Kimi", HomePaths: []string{".kimi"}},
	HarnessOpencode:    {Description: "OpenCode", XDGConfigPaths: []string{"opencode"}, LocalSharePaths: []string{"opencode"}, LocalStatePaths: []string{"opencode"}},
	HarnessPi:          {Description: "Pi", HomePaths: []string{".pi"}},
	HarnessQwen:        {Description: "Qwen Code", HomePaths: []string{".qwen"}},
}

HarnessMounts maps each known harness to its path configuration.

View Source
var WellKnownCaches = map[string][]CacheMount{
	"android-keys": {
		{Name: "android-keys", Description: "Android debug keystore and ADB keys", HostPath: "~/.android", ContainerPath: "/home/user/.android", Shallow: true},
	},
	"bun": {
		{Name: "bun", Description: "Bun package manager", HostPath: "~/.bun/install/cache", ContainerPath: "/home/user/.bun/install/cache"},
	},
	"cargo": {
		{Name: "cargo-registry", Description: "Rust cargo registry and git", HostPath: "~/.cargo/registry", ContainerPath: "/home/user/.cargo/registry"},
		{Name: "cargo-git", Description: "Rust cargo registry and git", HostPath: "~/.cargo/git", ContainerPath: "/home/user/.cargo/git"},
	},

	"go-mod": {
		{Name: "go-mod", Description: "Go module cache", HostPath: "~/go/pkg/mod", ContainerPath: "/home/user/go/pkg/mod"},
	},
	"gradle": {
		{Name: "gradle-caches", Description: "Gradle caches and wrapper", HostPath: "~/.gradle/caches", ContainerPath: "/home/user/.gradle/caches"},
		{Name: "gradle-wrapper", Description: "Gradle caches and wrapper", HostPath: "~/.gradle/wrapper/dists", ContainerPath: "/home/user/.gradle/wrapper/dists"},
	},
	"maven": {
		{Name: "maven", Description: "Maven repository", HostPath: "~/.m2/repository", ContainerPath: "/home/user/.m2/repository"},
	},
	"npm": {
		{Name: "npm", Description: "npm cache", HostPath: "~/.npm", ContainerPath: "/home/user/.npm"},
	},
	"pip": {
		{Name: "pip", Description: "Python pip cache", HostPath: "~/.cache/pip", ContainerPath: "/home/user/.cache/pip"},
	},
	"pnpm": {
		{Name: "pnpm", Description: "pnpm store", HostPath: "~/.local/share/pnpm/store", ContainerPath: "/home/user/.local/share/pnpm/store"},
	},
	"uv": {
		{Name: "uv", Description: "UV Python package manager", HostPath: "~/.cache/uv", ContainerPath: "/home/user/.cache/uv"},
	},
}

WellKnownCaches is the set of predefined build-tool caches, keyed by short name. Each name may expand to multiple [CacheMount]s (e.g. "cargo" covers both the registry index and git sources). HostPath values use "~/" as a prefix that Container.Launch resolves to the host user's home directory at runtime; ContainerPath values may use "~/" for the container user's home directory.

Functions

func FormatBytes added in v0.9.0

func FormatBytes(n int64) string

FormatBytes formats n bytes as a human-readable string (e.g. "1.2 GB").

func IsHomeRelativeHostPath added in v0.17.1

func IsHomeRelativeHostPath(p string) bool

IsHomeRelativeHostPath reports whether p is a path within the host user's home directory using md's supported home-relative syntax.

func ResolveContainerPath added in v0.13.5

func ResolveContainerPath(p string) string

ResolveContainerPath expands "~" or a leading "~/" to the container user's home directory; other paths are returned unchanged.

func ResolveHostPath added in v0.17.1

func ResolveHostPath(p, home string) string

ResolveHostPath expands "~" or a leading "~/" (or "~\" on Windows) to home; other paths are returned unchanged.

func ResolveMountTarget added in v0.17.1

func ResolveMountTarget(hostPath, containerPath string) (string, error)

ResolveMountTarget validates a host-to-container mapping and returns its effective container path. An empty container path mirrors a home-relative host path. The returned path retains "~" notation so ResolveContainerPath can expand it against the container user's home directory later.

Types

type AgentPaths added in v0.8.0

type AgentPaths struct {
	// Description is a short human-readable label for the harness (e.g.
	// "Claude Code"). Displayed in settings UI.
	Description string
	// ReadOnly mounts all paths in this group read-only.
	ReadOnly bool

	HomePaths       []string
	XDGConfigPaths  []string
	LocalSharePaths []string
	LocalStatePaths []string
}

AgentPaths groups the relative host directory paths for one or more agent harnesses. Paths under HomePaths are relative to $HOME, XDGConfigPaths to $XDG_CONFIG_HOME (~/.config), LocalSharePaths to $XDG_DATA_HOME (~/.local/share), and LocalStatePaths to $XDG_STATE_HOME (~/.local/state).

type CacheMount

type CacheMount struct {
	// Name identifies the cache in progress output and Docker build contexts.
	// It must match [a-z0-9][a-z0-9-]*, e.g. "go-mod".
	Name string
	// Description is a short human-readable label for the cache group (e.g.
	// "Go module cache"). Displayed in settings UI.
	Description string
	// HostPath is the absolute path on the host. "~" and "~/" resolve to the
	// host user's home directory.
	HostPath string
	// ContainerPath is the path inside the container. "~" and "~/" resolve to
	// the container user's home directory via [ResolveContainerPath]. When empty,
	// it mirrors a home-relative HostPath via [ResolveMountTarget].
	ContainerPath string
	// ReadOnly copies the cache into the image as root-owned, non-writable files.
	ReadOnly bool
	// Shallow copies only top-level files from HostPath, ignoring
	// subdirectories. Useful for directories like ~/.android where only a few
	// files (debug.keystore, adbkey) are needed but subdirectories (avd/,
	// cache/) are large and unwanted.
	Shallow bool
}

CacheMount defines a host directory to copy into the specialized container image. Well-known caches are defined in WellKnownCaches; custom caches can be constructed directly.

type Client

type Client struct {
	// 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

	// Runtime is the Docker or Podman runtime.
	Runtime containers.Runtime

	// Logger receives md package logs. It must be non-nil.
	Logger *slog.Logger

	// ControlMaster enables SSH ControlMaster connection multiplexing.
	// When true, SSH connections are shared via a persistent socket,
	// reducing connection overhead. Disabled by default because stale
	// sockets can cause connectivity issues that are hard to diagnose.
	ControlMaster bool

	// 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

	// DigestCacheTTL controls how long remote image digest lookups are cached.
	// When zero, caching is disabled and the registry is queried on every start.
	DigestCacheTTL time.Duration
	// contains filtered or unexported fields
}

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

func New

func New(logger *slog.Logger, rt containers.Runtime, stdout io.Writer) (*Client, error)

New creates a Client with global MD tool config and initialises SSH infrastructure (keys, authorized_keys, config.d include).

When logger is nil, slog.Default() is used. When runtime is nil, Docker or Podman is auto-detected.

func (*Client) AgentMounts added in v0.14.0

func (c *Client) AgentMounts(paths ...AgentPaths) ([]Mount, error)

AgentMounts returns runtime mounts for the provided agent path groups.

It creates the host directories before returning them. The shared agent directories are always included; pass values from HarnessMounts to include harness-specific directories.

func (*Client) BuildImage

func (c *Client) BuildImage(ctx context.Context, stdout, stderr io.Writer, platform Platform) (retErr error)

BuildImage builds the base Docker images locally for platform: first md-root-local, then md-user-local on top of it.

func (*Client) Close added in v0.16.0

func (c *Client) Close() error

Close implements io.Closer.

func (*Client) Container

func (c *Client) Container(repos ...Repo) (*Container, error)

Container returns a Container handle for the given repos. It populates ContainerPath on each repo from GitRoot if not already set (repos is mutated in place). The first repo is the primary. When called with no repos, the container has no associated git repository and a random name is generated automatically. It doesn't start it, it is just a reference.

func (*Client) Get added in v0.13.0

func (c *Client) Get(ctx context.Context, name string) (*Container, error)

Get returns a single Container by name, or an error if not found. Uses docker inspect for a targeted lookup.

func (*Client) List

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

List returns running md containers sorted by name.

func (*Client) PruneImages added in v0.9.0

func (c *Client) PruneImages(ctx context.Context, stdout, stderr io.Writer) ([]string, error)

PruneImages removes md-built images (specialized builds and fork snapshots) that are not used by any container. Returns the list of removed image names.

func (*Client) Warmup added in v0.9.0

func (c *Client) Warmup(ctx context.Context, stdout, stderr io.Writer, opts *WarmupOpts) (bool, error)

Warmup ensures the base image is pulled and a specialized image with current coding agents is built, without starting a container. Force rebuilds the image even when its inputs are unchanged. Returns true if a build ran.

type Container

type Container struct {
	// Client is the client that created this container.
	*Client

	// Logger receives logs for this container. It includes the cntr attribute.
	Logger *slog.Logger
	// Repos are the git repositories in this container. Repos[0] is the
	// primary; the rest are pushed alongside it. Each repo's ContainerPath
	// gives the absolute destination path.
	// Label: md.repos (base64-encoded JSON)
	Repos []Repo
	// 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
	// Labels contains all Docker/Podman labels observed on the container.
	Labels map[string]string
	// 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
	// Sudo indicates root access via sudo is enabled.
	// Label: md.sudo
	Sudo bool
	// SSHPort is the host port mapped to the container's SSH port.
	// Set by Launch; available immediately after Launch returns.
	SSHPort int32
	// VNCPort is the host port mapped to the container's VNC port, if display is enabled.
	// Set by Launch; available immediately after Launch returns. Zero if display is disabled.
	VNCPort int32
	// contains filtered or unexported fields
}

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.

Example
// Examples demonstrate using md as a library.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"strings"

	"github.com/caic-xyz/md"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	client, err := md.New(nil, nil, os.Stdout)
	if err != nil {
		_, _ = fmt.Fprintln(os.Stderr, err)
		return
	}
	base, err := client.Container(md.Repo{GitRoot: ".", Branches: []string{"main"}})
	if err != nil {
		_, _ = fmt.Fprintln(os.Stderr, err)
		return
	}

	run := &md.Container{
		Client: base.Client,
		Repos:  append([]md.Repo(nil), base.Repos...),
		Name:   base.Name + "-run",
	}
	opts := &md.StartOpts{}
	if err = run.Launch(ctx, os.Stdout, os.Stderr, opts); err != nil {
		_, _ = fmt.Fprintln(os.Stderr, err)
		return
	}
	if _, err = run.Connect(ctx, os.Stdout, os.Stderr, opts); err == nil {
		sshArgs := run.SSHCommand(nil, "cd "+shellQuote(run.Repos[0].ContainerPath)+" && go test ./...")
		cmd := exec.CommandContext(ctx, sshArgs[0], sshArgs[1:]...) //nolint:gosec // SSH target and command are built from md container state.
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		err = cmd.Run()
	}
	if err = errors.Join(err, run.Purge(ctx, os.Stdout, os.Stderr)); err != nil {
		_, _ = fmt.Fprintln(os.Stderr, err)
		return
	}
}

func shellQuote(s string) string {
	if s == "" {
		return "''"
	}
	for _, c := range s {
		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') &&
			c != '@' && c != '%' && c != '+' && c != '=' && c != ':' && c != ',' && c != '.' &&
			c != '/' && c != '-' && c != '_' {
			return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
		}
	}
	return s
}

func (*Container) Connect added in v0.9.0

func (c *Container) Connect(ctx context.Context, stdout, stderr io.Writer, opts *StartOpts) (*StartResult, error)

Connect waits for SSH, pushes repos into the container, and completes startup. Must be called after Launch. Container.Repos must have branches set before this call.

func (*Container) Diff

func (c *Container) Diff(ctx context.Context, stdout, stderr io.Writer, repoIdx int, opts *DiffOpts) error

Diff writes the checked-out container branch's changes, including untracked files, after verifying every mapped branch still matches the host.

By default it reports the changes not yet integrated into the host branch, using the point established by container creation or the last successful Push, Pull or Fork. Set DiffOpts.Full to report the whole branch instead.

opts may be nil, which selects the default of the zero DiffOpts.

When stdout is a terminal, a TTY is allocated so git's pager and colors work.

func (*Container) DiskUsage added in v0.9.0

func (c *Container) DiskUsage(ctx context.Context) (int64, error)

DiskUsage returns the writable container layer size in bytes via docker inspect --size. Works for both running and stopped containers.

func (*Container) Fetch

func (c *Container) Fetch(ctx context.Context, stdout, stderr io.Writer, repoIdx int, opts *FetchOpts) ([]FetchedBranch, error)

Fetch fetches the mapped branches of Repos[repoIdx] from the container and updates the host's remote-tracking refs without integrating them. It returns the exact branch tips fetched and does not change the base reported by Container.Diff.

The zero FetchOpts fetches only what the container already committed, which leaves its history and working tree untouched. Set FetchOpts.Commit to commit the container's pending changes first, so the host receives them too.

opts may be nil, which selects the default of the zero FetchOpts.

func (*Container) Fork added in v0.12.0

func (c *Container) Fork(ctx context.Context, stdout, stderr io.Writer, opts *ForkOpts) (_ *Container, retErr error)

Fork snapshots a running or stopped container and creates a new one where each mapped repository is checked out on a new primary branch.

The snapshot preserves the container's entire filesystem (installed packages, build artifacts, etc.) while giving each repo a fresh primary branch that diverges from the source container's working state. Extra mapped branches keep their original names.

Branch naming: the caller supplies the full repo set via ForkOpts.Repos, each entry naming its DestPrimary. Fork uses those names verbatim and only rejects a name the repo already maps, which it would destroy. Allocating a free name and avoiding other containers' branches belongs to the caller, symmetric with Launch, where the caller owns branch names too.

If setup fails, Fork removes the partial container and restores or deletes destination host branches that still point to the commits written by this attempt. Branches moved concurrently are preserved and reported.

func (*Container) Inspect added in v0.14.2

func (c *Container) Inspect(ctx context.Context) (*InspectInfo, error)

Inspect returns detailed observed runtime configuration for the container.

func (*Container) Launch added in v0.9.0

func (c *Container) Launch(ctx context.Context, stdout, stderr io.Writer, opts *StartOpts) (retErr error)

Launch prepares the image and starts the Docker container. It does NOT wait for SSH to become ready — call Connect to complete startup once the container's repos have their branches set (e.g. after concurrent branch allocation).

func (*Container) Pull

func (c *Container) Pull(ctx context.Context, stdout, stderr io.Writer, repoIdx int, opts *PullOpts) error

Pull fetches changes from the container and integrates all mapped branches into the local repository.

opts controls AI commit message generation. Pass nil to use a default message.

func (*Container) Purge added in v0.9.0

func (c *Container) Purge(ctx context.Context, stdout, stderr io.Writer) error

Purge stops and removes the container, cleaning up SSH config and git remotes.

func (*Container) Push

func (c *Container) Push(ctx context.Context, stdout, stderr io.Writer, repoIdx int) (string, error)

Push force-pushes local state for Repos[repoIdx] into the container, saving a backup of the container state and returning the backup branch name. All branches in Branches are synced.

func (*Container) Revive added in v0.9.0

func (c *Container) Revive(ctx context.Context, stdout, stderr io.Writer) error

Revive restarts a stopped (exited) container. It restores missing local upstream branches from the preserved container, validates git remotes, runs `docker start`, re-queries the SSH port (which changes on restart), rewrites the SSH config, and waits for SSH to become ready. It does NOT reset working branches or send .env — the container's filesystem is preserved across stop/start.

func (*Container) SSHCommand added in v0.13.2

func (c *Container) SSHCommand(opts []string, cmd string) []string

SSHCommand returns SSH command args for this container. opts are SSH flags (e.g. "-q", "-t"); cmd is the remote command. The container name is always included as the SSH host target. If cmd is empty, only the base SSH args and host are returned (for interactive sessions).

func (*Container) Status added in v0.13.3

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

Status returns the Docker container state (e.g. "running", "exited", ""). Returns empty string when the container does not exist.

func (*Container) Stop added in v0.9.0

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

Stop stops the container without removing it. The container can be restarted later with Revive. SSH config is preserved (Revive rewrites it with the new port), but the ControlMaster socket is removed to prevent stale connections from interfering with subsequent SSH commands.

func (*Container) SudoPassword added in v0.13.0

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

SudoPassword retrieves the random sudo password set at container startup, or "" if no password was configured.

For containers created by Launch, the password is cached in-memory. For containers loaded from List (e.g. md sudo-password), it falls back to reading the md.sudo-password Docker label.

func (*Container) SyncDefaultBranch

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

SyncDefaultBranch mirrors the host's cached remote branches for Repos[repoIdx] into the container. The historical name is retained for API compatibility.

func (*Container) TailscaleFQDN

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

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

type DiffOpts added in v0.18.0

type DiffOpts struct {
	// Args are extra arguments forwarded to git diff, for example "--stat".
	Args []string
	// Full reports the whole branch, from the merge base with its upstream,
	// instead of only the work not yet integrated into the host branch.
	Full bool
}

DiffOpts configures Container.Diff. Its zero value reports container work not yet integrated into the host branch.

type FetchOpts added in v0.18.0

type FetchOpts struct {
	// Provider generates the commit message when Commit is set. A nil Provider
	// uses a fixed message.
	Provider genai.Provider
	// ContextTokens is the selected model's context window. Zero uses the
	// commit-message generator default.
	ContextTokens int
	// Commit stages and commits the container's pending changes before
	// fetching, so the host receives them as well. Leave it unset to observe
	// the container without changing its history or its working tree.
	Commit bool
	// NoVerify passes --no-verify to the container commit, skipping the
	// pre-commit and commit-msg hooks. It has no effect unless Commit is set.
	NoVerify bool
}

FetchOpts configures Container.Fetch. Its zero value fetches only what the container already committed.

type FetchedBranch added in v0.18.0

type FetchedBranch struct {
	// BranchName is the short local branch name in the container, such as
	// "main" or "caic-1".
	BranchName string
	// CommitHash is the full Git object ID of the fetched branch tip.
	CommitHash string
}

FetchedBranch identifies an exact container branch tip observed by Container.Fetch.

type ForkOpts added in v0.12.0

type ForkOpts struct {
	// Repos is the full set of repositories the fork should contain. It must
	// include an entry for every repo in the source container (identified by
	// GitRoot); additional entries are new repos initialized in the fork and
	// pushed from the host. Fork fails if a source-container repo is missing, if
	// GitRoots are duplicated, or if a DestPrimary is empty.
	Repos []ForkRepo
	// Display enables X11/VNC virtual display on the forked container.
	Display bool
	// Tailscale enables Tailscale networking on the forked container.
	Tailscale bool
	// USB enables USB device passthrough on the forked container.
	USB bool
	// Sudo enables root access via sudo on the forked container.
	Sudo bool
	// Labels are additional Docker labels (key=value) applied to the forked container.
	Labels []string
	// Quiet suppresses informational output.
	Quiet bool
	// ExtraEnv holds environment updates to inject into the container's ~/.env.
	// Entries use KEY=VALUE to set a value or KEY= to remove one. Values are
	// shell-quoted before writing, so they may contain spaces and newlines.
	ExtraEnv []string
	// Mounts lists host directories to bind-mount into the running container.
	// Missing host directories are rejected before the runtime is invoked.
	Mounts []Mount
	// MaxCPUs limits the number of CPU cores the forked container may use.
	// Passed as --cpus to docker/podman. Zero means no limit.
	// Use [DefaultMaxCPUs] to calculate a default from the runtime CPU count.
	MaxCPUs int
	// ExtraRunArgs are additional arguments passed verbatim to the
	// container runtime's "run" command. Not portable across runtimes.
	ExtraRunArgs []string
}

ForkOpts configures a container fork operation.

type ForkRepo added in v0.17.0

type ForkRepo struct {
	// GitRoot is the absolute path of the host git repository. Fork matches it
	// against the source container's repos: a match is re-branched from the
	// snapshot, a non-match is initialized in the fork and pushed from the host.
	GitRoot string
	// SourceBranches are the branches to carry, primary first. For a repo present
	// in the source container it must equal the branches recorded on that repo
	// (Fork validates this). For a new repo it is the host branches to push; if
	// empty, it defaults to the repo's default upstream branch.
	SourceBranches []string
	// ContainerPath is the absolute container path to mount a new repo at. It is
	// used only for repos not already in the source container (existing repos
	// keep the snapshot's mount path). When empty, it defaults to
	// /home/user/src/<basename>.
	ContainerPath string
	// TagRegexp selects tags for a new repo. Existing source repos retain their
	// persisted tag selection.
	TagRegexp string
	// DestPrimary is the fork's primary branch, created from SourceBranches[0].
	// It must not be one of the repo's mapped branches, since Fork force-resets
	// the destination host branch. Fork does not check it against other
	// containers' branches or unrelated local refs, so the caller still owns
	// uniqueness there. Additional (non-primary) branches keep their source
	// names.
	DestPrimary string
}

ForkRepo describes one repository in a fork and the branches it carries.

type Harness added in v0.8.0

type Harness string

Harness identifies an agent harness whose config directories are mounted into a container.

const (
	HarnessAmp         Harness = "amp"
	HarnessAndroid     Harness = "android"
	HarnessAntigravity Harness = "antigravity"
	HarnessClaude      Harness = "claude"
	HarnessCodex       Harness = "codex"
	HarnessGoose       Harness = "goose"
	HarnessKilo        Harness = "kilo"
	HarnessKimi        Harness = "kimi"
	HarnessOpencode    Harness = "opencode"
	HarnessPi          Harness = "pi"
	HarnessQwen        Harness = "qwen"
)

Known agent harnesses.

type InspectInfo added in v0.14.2

type InspectInfo struct {
	Runtime      string
	ID           string
	Name         string
	State        string
	ImageRef     string
	ImageID      string
	Platform     string
	OS           string
	Architecture string
	CPULimit     float64 // Explicit quota in CPU units; zero means unrestricted.
	CPUSet       []int   // Sorted configured logical CPU IDs; empty means inherited.
	Mounts       []Mount
	Caches       []CacheMount
}

InspectInfo describes observed Docker/Podman runtime configuration for a container.

type Mount added in v0.14.0

type Mount struct {
	// HostPath is the absolute path on the host. "~" and "~/" resolve to the
	// host user's home directory.
	HostPath string
	// ContainerPath is the path inside the container. "~" and "~/" resolve to
	// the container user's home directory via [ResolveContainerPath]. When empty,
	// it mirrors a home-relative HostPath via [ResolveMountTarget].
	ContainerPath string
	// ReadOnly mounts the host path read-only.
	ReadOnly bool
}

Mount defines a host directory to bind-mount into the running container.

type Platform added in v0.14.0

type Platform string

Platform is a Linux container platform.

const (
	// PlatformDefault uses the host's native Linux container platform.
	PlatformDefault Platform = ""
	// PlatformLinuxARM64 is the Linux arm64 container platform.
	PlatformLinuxARM64 Platform = "linux/arm64"
	// PlatformLinuxAMD64 is the Linux amd64 container platform.
	PlatformLinuxAMD64 Platform = "linux/amd64"
)

func DefaultPlatform added in v0.14.0

func DefaultPlatform() Platform

DefaultPlatform returns the host's native Linux container platform.

func (Platform) Architecture added in v0.14.0

func (p Platform) Architecture() (string, error)

Architecture returns the platform architecture component.

func (Platform) Resolve added in v0.14.0

func (p Platform) Resolve() Platform

Resolve returns the host's native Linux container platform when p is empty.

func (Platform) String added in v0.14.0

func (p Platform) String() string

String returns p as a Docker platform string.

func (Platform) Validate added in v0.14.0

func (p Platform) Validate() error

Validate returns an error unless p is a supported Linux container platform or PlatformDefault.

type PullOpts added in v0.18.1

type PullOpts struct {
	// Provider generates the commit message. A nil Provider uses a fixed message.
	Provider genai.Provider
	// ContextTokens is the selected model's context window. Zero uses the
	// commit-message generator default.
	ContextTokens int
	// NoVerify passes --no-verify to the container commit, skipping the
	// pre-commit and commit-msg hooks.
	NoVerify bool
}

PullOpts configures Container.Pull.

type Repo added in v0.9.0

type Repo struct {
	// GitRoot is the absolute path to the git repository root on the host.
	GitRoot string `json:"git_root"`
	// Branches lists the git branches to push into the container.
	// Branches[0] is the primary branch (checked out in the working tree,
	// used for container naming). Branches[1:] are also available as local
	// branches. Each branch tracks the same upstream as its host branch.
	Branches []string `json:"branches"`
	// ContainerPath is the absolute destination path inside the
	// container, e.g. "/home/user/src/github/caic". When empty,
	// populateContainerPath fills it from filepath.Base(GitRoot).
	// When two repos share the same basename, resolveContainerPaths
	// disambiguates using relative paths from /home/user/src.
	// Callers may set it explicitly to override.
	ContainerPath string `json:"container_path,omitempty"`
	// Remotes lists the host remotes whose cached branches are mirrored into the container.
	Remotes []string `json:"remotes,omitempty"`
	// TagRegexp selects host tags to mirror into the container. Empty selects no tags.
	TagRegexp string `json:"tag_regexp,omitempty"`
	// DefaultRemote is the host's default git remote. When empty, the upstream
	// remote tracked by local main or master takes precedence over origin.
	DefaultRemote string `json:"default_remote,omitempty"`
	// DefaultBranch is the default branch for DefaultRemote.
	DefaultBranch string `json:"default_branch,omitempty"`
}

Repo describes a git repository to push into a container.

func (*Repo) ResolveDefaults added in v0.19.0

func (r *Repo) ResolveDefaults(ctx context.Context, logger *slog.Logger) error

ResolveDefaults populates Remotes, DefaultRemote, and DefaultBranch if not already set. It reads the host checkout without modifying its refs or configuration.

func (*Repo) UnmarshalJSON added in v0.16.0

func (r *Repo) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes Repo, including legacy persisted labels.

func (*Repo) Validate added in v0.13.2

func (r *Repo) Validate() error

Validate returns an error for invalid repo fields.

type Runtime added in v0.18.2

type Runtime string

Runtime identifies a workspace execution backend. RuntimeAuto leaves backend selection to md's normal detection.

const (
	RuntimeAuto   Runtime = ""
	RuntimeDocker Runtime = "docker"
	RuntimePodman Runtime = "podman"
)

Runtime values identify supported workspace execution backends.

func (*Runtime) Set added in v0.18.2

func (r *Runtime) Set(value string) error

Set implements flag.Value, rejecting unknown runtime names.

func (*Runtime) String added in v0.18.2

func (r *Runtime) String() string

String implements flag.Value.

func (*Runtime) Valid added in v0.18.2

func (r *Runtime) Valid() bool

Valid reports whether r is an execution backend or the automatic default.

type StartOpts

type StartOpts struct {
	// BaseImage is the full Docker image reference (e.g.
	// "ghcr.io/caic-xyz/md-user:v0.7.1" or "myregistry/custom:tag"). When empty,
	// DefaultBaseImage is used.
	BaseImage string
	// Platform is the Linux container platform, e.g. "linux/amd64" or
	// "linux/arm64". Empty means use the host's native platform.
	Platform string
	// GitObjectCopyMinBytes is the minimum object store size for copying Git
	// objects into a new container before the initial push. Zero uses the
	// default, -1 disables copying, and 1 includes every nonempty object store.
	GitObjectCopyMinBytes int64
	// 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
	// Sudo enables password-based sudo for the user account. A random
	// password is generated per container and stored as a container label;
	// retrieve it with `md sudo-password`. Defaults to false.
	Sudo bool
	// Caches lists host directories to COPY into the image at build time.
	// Use well-known names from [WellKnownCaches] or construct [CacheMount]
	// values directly. Paths that do not exist on the host are silently skipped.
	Caches []CacheMount
	// Mounts lists host directories to bind-mount into the running container.
	// Missing host directories are rejected before the runtime is invoked.
	Mounts []Mount
	// Labels are additional Docker labels (key=value) applied to the container.
	Labels []string
	// Quiet suppresses informational output during startup.
	Quiet bool
	// ExtraEnv holds environment updates to inject into the container's ~/.env.
	// Entries use KEY=VALUE to set a value or KEY= to remove one. Values are
	// shell-quoted before writing, so they may contain spaces and newlines.
	ExtraEnv []string
	// MaxCPUs limits the number of CPU cores the container may use.
	// Passed as --cpus to docker/podman. Zero means no limit.
	// Use [DefaultMaxCPUs] to calculate a default from the runtime CPU count.
	MaxCPUs int
	// ExtraRunArgs are additional arguments passed verbatim to the
	// container runtime's "run" command. Not portable across runtimes.
	ExtraRunArgs []string
	// contains filtered or unexported fields
}

StartOpts configures container startup.

type StartResult

type StartResult struct {
	// 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 Tailscale information from Connect. Port information is available on Container directly (SSHPort, VNCPort) after Launch returns.

type WarmupOpts added in v0.9.0

type WarmupOpts struct {
	// BaseImage is the full Docker image reference. When empty,
	// DefaultBaseImage+":latest" is used.
	BaseImage string
	// Platform is the Linux container platform. Empty means use the host's
	// native platform.
	Platform string
	// Caches lists host directories to COPY into the image at build time.
	Caches []CacheMount
	// Quiet suppresses informational output.
	Quiet bool
	// Force rebuilds the specialized image even when its inputs are unchanged.
	Force bool
}

WarmupOpts configures base image warmup.

Directories

Path Synopsis
cmd
md command
Package main implements the md CLI for isolated Docker development containers.
Package main implements the md CLI for isolated Docker development containers.
Package containers wraps Docker and Podman command-line runtimes.
Package containers wraps Docker and Podman command-line runtimes.
Package git provides git utility functions for repository introspection, branch management, and pushing.
Package git 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