Skip to main content

Architecture

System Overview

Clawker is a CLI tool that manages isolated Docker containers for AI coding agents. It has three main subsystems that work together:
┌──────────────────────────────────────────────────────────────────────┐
│  CLI Layer                                                            │
│  cmd/clawker → internal/clawker → internal/cmd/root                   │
│  10 command groups, 50+ subcommands (Cobra)                           │
│  Factory DI pattern (gh CLI style)                                    │
└──────┬────────────────────────┬────────────────────┬─────────────────┘
       │                        │                    │
       ▼                        ▼                    ▼
┌──────────────┐  ┌─────────────────────┐  ┌───────────────────────┐
│ Container    │  │ Configuration       │  │ Security              │
│ Subsystem    │  │ Subsystem           │  │ Subsystem             │
│              │  │                     │  │                       │
│ docker/      │  │ storage/ (engine)   │  │ firewall/ (Envoy+DNS) │
│ workspace/   │  │ config/ (project)   │  │ hostproxy/ (auth)     │
│ containerfs/ │  │ config/ (settings)  │  │ socketbridge/ (SSH)   │
│ bundler/     │  │ project/ (registry) │  │ keyring/ (creds)      │
│              │  │ storeui/ (TUI edit) │  │                       │
│ pkg/whail    │  │                     │  │                       │
│ (engine lib) │  │                     │  │                       │
└──────┬───────┘  └─────────────────────┘  └───────────────────────┘


  moby/moby (Docker SDK)

CLI Command Structure

Commands are organized under internal/cmd/ with subpackages per subcommand:
Command GroupSubcommandsPurpose
containerlist, run, start, stop, kill, exec, attach, logs, inspect, cp, pause, unpause, restart, rename, remove, stats, top, update, wait, createDocker container management
imagelist, build, inspect, remove, pruneImage lifecycle
volumelist, create, inspect, remove, pruneVolume management
networklist, create, inspect, remove, pruneNetwork management
projectinit, register, list, info, edit, removeProject registration and config
worktreeadd, list, prune, removeGit worktree management
firewallstatus, list, add, remove, reload, up, down, enable, disable, bypass, rotate-caEgress firewall control
monitorinit, up, down, statusObservability stack (Grafana, Prometheus)
loopiterate, status, tasks, resetAutonomous agent loops
settingseditUser settings TUI editor
Top-level shortcuts: initproject init, buildimage build, run/startcontainer run/start, generate, version Shared domain logic (container creation, loop orchestration) lives in shared/ subpackages within each command group — not in library packages.

Package Dependency Graph

Packages follow a strict DAG with no cycles. Verified via goda.

Leaf Packages (zero internal imports)

Importable by anyone. Depend only on stdlib or external libraries.
PackagePurpose
storageGeneric layered YAML store engine (Store[T]): discovery, merge, provenance, atomic write
gitGit operations, worktree management (go-git)
loggerStruct-based zerolog (file rotation + optional OTEL bridge)
textPure ANSI-aware text utilities (Truncate, PadRight, StripANSI)
termTerminal capabilities + raw mode (sole x/term gateway)
signalsOS signal utilities (SetupSignalContext, ResizeHandler)
buildBuild-time metadata (version, date via ldflags)
updateBackground GitHub release checker (24h cached)
keyringCredential storage service
pkg/whailReusable Docker engine library with label-based isolation

Foundation Packages (import leaves only)

Universally imported infrastructure. Their imports are leaf-only.
PackageImportsPurpose
configstorageConfig loading/validation, schema types, path resolution. Composes Store[Project] + Store[Settings]
iostreamsterm, textI/O streams, TTY detection, colors, styles, spinners, progress

Domain Packages (import leaves + foundation)

Core business logic. Import leaves and foundation packages only.
PackageImportsPurpose
projectconfig, git, logger, storage, textProject registration, worktree lifecycle, registry CRUD
bundlerconfig + own subpkgsDockerfile generation, content hashing, semver, npm registry
tuiiostreams, textBubbleTea models, viewports, panels, progress display
prompteriostreamsInteractive prompts with TTY/CI awareness
storeuiiostreams, storage, tuiGeneric TUI editor for Store[T] instances
firewallconfig, logger, storageEnvoy+CoreDNS firewall: daemon, config gen, PKI, rules
hostproxyconfig, loggerHost proxy for container-to-host communication
socketbridgeconfig, loggerSSH/GPG agent forwarding via muxrpc
containerfsconfig, keyring, loggerHost config preparation for container init
monitorconfigObservability stack templates (Grafana, Prometheus)
docsconfig, storageCLI documentation generation

Composite Packages (import domain packages)

Higher-level subsystems that compose domain packages.
PackageKey ImportsPurpose
dockerbundler, config, pkg/whail, pkg/whail/buildkitClawker middleware wrapping whail with labels/naming
workspaceconfig, docker, loggerBind vs Snapshot strategies
cmdutilconfig, docker, iostreams, tui, + type imports for FactoryFactory struct (DI container), error types, arg validators

Import Rules

  OK:  foundation → leaf           (config → storage)
  OK:  domain → leaf               (firewall → storage)
  OK:  domain → foundation         (project → config)
  OK:  composite → domain          (docker → bundler)

  BAD: leaf → anything internal    (storage must never import config)
  BAD: foundation ↔ foundation     (config must never import iostreams)
  BAD: any cycle                   (A → B → A is always wrong)

Configuration and Storage

Three packages form the configuration subsystem:
  • storage (leaf) — Generic Store[T] engine. Handles file discovery (static paths + walk-up), YAML loading with migrations, N-way merge with provenance tracking, and atomic writes. Zero domain knowledge.
  • config — Thin domain wrapper composing Store[Project] + Store[Settings]. Exposes the Config interface with schema types, path helpers, and ~40 accessor methods.
  • project — Project domain layer. Owns projects.yaml registry via its own Store[ProjectRegistry]. Handles registration CRUD, path resolution, worktree lifecycle.
Commands access config through the Config interface and ProjectManager interface — never storage directly.

Dependency Injection: The Factory Pattern

Follows the GitHub CLI’s three-layer pattern:
  1. Wiring (internal/cmd/factory/) — Creates *cmdutil.Factory with all deps as sync.Once closures. Called once at entry point. Tests never import this.
  2. Contract (internal/cmdutil/) — Factory is a pure struct with closure fields. Eager: Version, IOStreams, TUI. Lazy: Config, Client, ProjectManager, GitManager, HostProxy, SocketBridge, Firewall, Logger, Prompter.
  3. Consumers (internal/cmd/*/) — Cherry-pick Factory closures into per-command Options structs. Run functions accept *Options only.
func NewCmdStop(f *cmdutil.Factory, runF func(*StopOptions) error) *cobra.Command {
    opts := &StopOptions{
        IOStreams: f.IOStreams,
        Client:   f.Client,  // closure, not call
    }
}

func stopRun(ctx context.Context, opts *StopOptions) error {
    client, err := opts.Client(ctx)  // resolved lazily here
}

Key Abstractions

AbstractionPackagePurpose
storage.Store[T]internal/storageGeneric layered YAML store with merge, provenance, and atomic write
config.Configinternal/configTyped config interface composing Store[Project] + Store[Settings]
project.ProjectManagerinternal/projectProject identity, registration CRUD, worktree orchestration
whail.Enginepkg/whailReusable Docker engine with label-based resource isolation
docker.Clientinternal/dockerClawker middleware (labels, naming) wrapping whail
cmdutil.Factoryinternal/cmdutilDI struct (closure fields); constructor in cmd/factory
CreateContainer()internal/cmd/container/sharedSingle entry point for container creation
firewall.FirewallManagerinternal/firewallEnvoy+CoreDNS firewall lifecycle, rules, bypass
tui.TUIinternal/tuiFactory noun for presentation layer

Container Naming and Labels

Container names: clawker.project.agent (3-segment) or clawker.agent (2-segment when project is empty) Volume names: clawker.project.agent-purpose (purposes: workspace, config, history) Labels (all under dev.clawker.*):
LabelPurpose
managedtrue — authoritative ownership marker
projectProject name (omitted when empty)
agentAgent name
versionClawker version
imageSource image reference
Labels are authoritative for resource ownership. Names are for human readability. Clawker refuses to operate on resources without dev.clawker.managed=true.

Presentation Layer

Commands follow a 4-scenario output model:
ScenarioDescriptionPackages
StaticPrint and doneiostreams + fmt
Static-interactiveOutput with y/n promptsiostreams + prompter
Live-displayContinuous rendering, no inputiostreams + tui
Live-interactiveFull keyboard input, navigationiostreams + tui
Import boundaries (enforced):
  • Only iostreams imports lipgloss
  • Only tui imports bubbletea/bubbles
  • Only term imports golang.org/x/term