Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@ homebrew_casks:
ids:
- darwin
caveats: |
Headjack requires a container runtime (Podman, Docker, or Apple Container).
Headjack requires a container runtime (Podman, Docker).
Get started: hjk auth claude && hjk run feat/my-feature --agent claude "Your task"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Full documentation is available at [headjack.gilman.io](https://headjack.gilman.
## Requirements

- macOS or Linux
- [Docker](https://www.docker.com/) (default), [Podman](https://podman.io/), or Apple Containerization (macOS 26+)
- [Docker](https://www.docker.com/) (default) or [Podman](https://podman.io/)
- Git

## License
Expand Down
12 changes: 4 additions & 8 deletions docs/designs/devcontainer-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This document describes the design for integrating devcontainer support into Hea

Headjack uses a layered architecture for container management:

1. **Runtime Interface** (`internal/container/container.go`): Abstracts container operations (Run, Exec, Stop, etc.) across Docker, Podman, and Apple Containerization
1. **Runtime Interface** (`internal/container/container.go`): Abstracts container operations (Run, Exec, Stop, etc.) across Docker and Podman
2. **Instance Manager** (`internal/instance/manager.go`): Orchestrates instance lifecycle, creating worktrees, containers, and managing sessions
3. **Image Labels**: Runtime configuration is extracted from OCI image labels (`io.headjack.init`, `io.headjack.podman.flags`, etc.)
4. **Configuration Merging**: Flags from config files take precedence over image labels
Expand Down Expand Up @@ -83,9 +83,6 @@ The devcontainer CLI supports Docker and Podman via the `--docker-path` flag:
|---------|-----------|-------|
| Docker | ✅ | Native support |
| Podman | ✅ | Via `--docker-path podman` |
| Apple Containerization | ❌ | Not Docker-compatible |

Attempting to use devcontainer mode with Apple Containerization will result in an error.

### RunConfig Extension

Expand All @@ -103,7 +100,7 @@ type RunConfig struct {
}
```

- Vanilla runtimes (Docker, Podman, Apple) ignore `WorkspaceFolder`
- Vanilla runtimes (Docker, Podman) ignore `WorkspaceFolder`
- `DevcontainerRuntime` uses `WorkspaceFolder` and ignores `Image`

### DevcontainerRuntime Implementation
Expand Down Expand Up @@ -237,11 +234,10 @@ This matches user expectations from VS Code, GitHub Codespaces, and DevPod.
│ CLI Layer (cmd/run.go) │
│ │ │
│ ├── --image flag passed? │
│ │ └── Yes: Use vanilla runtime (Docker/Podman/Apple)
│ │ └── Yes: Use vanilla runtime (Docker/Podman)
│ │ │
│ └── devcontainer.json exists? │
│ ├── Yes + Docker/Podman: Use DevcontainerRuntime │
│ ├── Yes + Apple: Error (not supported) │
│ ├── Yes: Use DevcontainerRuntime │
│ └── No: Use vanilla runtime with default image │
│ │
│ Instance Manager receives containerRuntime interface │
Expand Down
15 changes: 9 additions & 6 deletions docs/docs/decisions/adr-002-apple-containerization.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ description: Decision to use Apple Containerization Framework for agent isolatio

## Status

Accepted
Superseded

:::info Supersession Note
Apple Containerization support was removed from Headjack. The project now supports only Docker and Podman runtimes for cross-platform compatibility. This ADR is preserved for historical context.
:::

## Context

Expand Down Expand Up @@ -82,20 +86,19 @@ Use **Apple Containerization Framework** as the isolation technology for Headjac
- By adopting early, we participate in the framework's growth through usage and bug reports
- The iptables-legacy workaround for Docker-in-Docker is stable but adds base image complexity

## Addendum: Multi-Runtime Support
## Addendum: Runtime Evolution

This ADR originally established Apple Containerization Framework as the isolation technology. After further development, Headjack evolved to support multiple runtimes and eventually removed Apple Containerization support in favor of Docker and Podman for cross-platform compatibility.

While Apple Containerization Framework remains the recommended runtime for its superior isolation properties, Headjack now supports multiple container runtimes to accommodate different user preferences and environments:
Current supported runtimes:

| Runtime | Configuration | Binary | Notes |
|---------|--------------|--------|-------|
| Docker | `runtime.name: docker` | `docker` | Default runtime. Cross-platform, widely available. |
| Apple | `runtime.name: apple` | `container` | Recommended for macOS 26+. VM-level isolation. |
| Podman | `runtime.name: podman` | `podman` | Daemonless alternative. |

Users can configure their preferred runtime via:

```bash
hjk config runtime.name docker
```

This flexibility allows teams to use familiar tooling while still benefiting from Headjack's instance and session management.
2 changes: 1 addition & 1 deletion docs/docs/decisions/adr-003-go-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Use **Go** as the implementation language for Headjack.

- **Verbosity**: More boilerplate than scripting languages
- **Error handling**: Explicit error checking adds code volume
- **No Apple Containerization SDK**: Must shell out regardless (not a Go-specific limitation)
- **No container CLI SDK**: Must shell out to Docker/Podman CLI (not a Go-specific limitation)

### Neutral

Expand Down
6 changes: 3 additions & 3 deletions docs/docs/decisions/adr-005-no-gpg-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ Accepted

## Context

Developers commonly use GPG to sign git commits. When running agents in isolated Apple Containerization instances, the host's GPG keys and agent are not directly accessible.
Developers commonly use GPG to sign git commits. When running agents in isolated container instances, the host's GPG keys and agent are not directly accessible.

We investigated two approaches to enable GPG signing from within containers:

### Option 1: GPG Agent Forwarding via TCP Bridge

GPG agent forwarding works by proxying the host's `gpg-agent` socket into the container. However, Unix sockets don't cross VM boundaries (each Apple Container is a separate VM).
GPG agent forwarding works by proxying the host's `gpg-agent` socket into the container. This requires mounting the socket into the container.

A workaround exists using `socat` to bridge Unix socket → TCP on the host, then TCP → Unix socket in the container. This was validated empirically and works, including with hardware tokens (Yubikey).
A TCP bridge approach using `socat` to bridge Unix socket → TCP on the host, then TCP → Unix socket in the container was validated empirically and works, including with hardware tokens (Yubikey).

**Complexity:**
- Requires `socat` installed on host
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/decisions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ An Architecture Decision Record (ADR) is a document that captures an important a
| ADR | Title | Status |
|-----|-------|--------|
| [ADR-001](./adr-001-macos-only) | Initial macOS-Only Platform Support | Superseded |
| [ADR-002](./adr-002-apple-containerization) | Apple Containerization Framework | Accepted |
| [ADR-002](./adr-002-apple-containerization) | Apple Containerization Framework | Superseded |
| [ADR-003](./adr-003-go-language) | Go as Implementation Language | Accepted |
| [ADR-004](./adr-004-cli-agents) | CLI-Based Agents over API-Based | Accepted |
| [ADR-005](./adr-005-no-gpg-support) | Defer GPG Commit Signing Support | Accepted |
Expand All @@ -27,7 +27,7 @@ An Architecture Decision Record (ADR) is a document that captures an important a

These decisions reflect several key themes in Headjack's design:

- **Simplicity over generality**: initial single-platform scope (macOS), OCI images only, CLI agents only
- **Leverage existing ecosystems**: Apple Containerization, Go CLI patterns, standard OCI tooling
- **Simplicity over generality**: OCI images only, CLI agents only
- **Leverage existing ecosystems**: Docker/Podman, Go CLI patterns, standard OCI tooling
- **Defer complexity**: GPG support deferred, Nix support left to users
- **Optimize for the common case**: Subscription-based agents, opinionated base images
2 changes: 1 addition & 1 deletion docs/docs/explanation/image-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: OCI images approach vs alternatives like Nix

# Image Customization

Headjack runs agents in containers, and those containers need the right tools installed. How do you customize the environment when your project needs specific languages, frameworks, or system packages? Headjack answers this with standard OCI images, delegating all customization to Docker, Podman, or Apple Container tooling you already know.
Headjack runs agents in containers, and those containers need the right tools installed. How do you customize the environment when your project needs specific languages, frameworks, or system packages? Headjack answers this with standard OCI images, delegating all customization to Docker or Podman tooling you already know.

## The Customization Problem

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/how-to/build-custom-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ hjk run feat/auth --base ghcr.io/gilmanlab/headjack:dind

### Prerequisites

- Docker, Podman, or Apple Container installed
- Docker or Podman installed
- Familiarity with Dockerfile syntax

### Create a Dockerfile
Expand Down Expand Up @@ -85,7 +85,7 @@ podman build -t my-custom-headjack:latest -f Dockerfile.headjack .

### Build for multiple architectures

For teams with both Intel and Apple Silicon Macs (using Docker buildx):
For teams with both Intel and ARM Macs (using Docker buildx):

```bash
docker buildx build \
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/how-to/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ This guide is being written. Check back soon!

## Prerequisites

- macOS or Linux (Apple Containerization requires macOS 26+)
- macOS or Linux
- Git
- Container runtime (Docker, Podman, or Apple Container)
- Container runtime (Docker or Podman)

## Installation Steps

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Common configuration keys:
| `storage.worktrees` | string | Directory for git worktrees |
| `storage.catalog` | string | Path to the instance catalog file |
| `storage.logs` | string | Directory for session logs |
| `runtime.name` | string | Container runtime (`podman`, `apple`, `docker`) |
| `runtime.name` | string | Container runtime (`podman`, `docker`) |

## Configuration File

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Container runtime configuration.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `runtime.name` | string | `docker` | Container runtime to use. Valid values: `podman`, `apple`, `docker`. |
| `runtime.name` | string | `docker` | Container runtime to use. Valid values: `podman`, `docker`. |
| `runtime.flags` | map[string]any | `{}` | Additional flags to pass to the container runtime. |

## Example Configuration
Expand Down Expand Up @@ -145,7 +145,7 @@ Headjack validates configuration values when loading and setting them:

- `default.agent` must be one of: `claude`, `gemini`, `codex` (or empty)
- `default.base_image` is required and cannot be empty
- `runtime.name` must be one of: `podman`, `apple`, `docker`
- `runtime.name` must be one of: `podman`, `docker`
- All storage paths are required

Invalid values will result in an error message describing the validation failure.
31 changes: 1 addition & 30 deletions docs/docs/reference/images/labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,32 +132,6 @@ LABEL io.headjack.docker.flags="privileged=true"
| `systemd` | `privileged=true cgroupns=host volume=/sys/fs/cgroup:/sys/fs/cgroup:rw` |
| `dind` | Inherited from `systemd` |

---

### io.headjack.apple.flags

Reserved for Apple Containerization Framework-specific flags.

| Property | Value |
|----------|-------|
| Key | `io.headjack.apple.flags` |
| Value type | String (space-separated key=value pairs) |
| Default | None |

#### Description

This label is reserved for specifying flags specific to the Apple Containerization Framework. It follows the same format as `io.headjack.podman.flags`.

#### Example

```dockerfile
LABEL io.headjack.apple.flags="rosetta=true"
```

#### Usage in Official Images

Not currently used in official images.

## Building Custom Images

When building custom images that extend the official Headjack images, labels are not automatically inherited. You must explicitly set any labels you need.
Expand Down Expand Up @@ -203,17 +177,14 @@ LABEL io.headjack.init="/usr/local/bin/init.sh"

## Label Inspection

You can inspect image labels using Docker, Podman, or Apple Container:
You can inspect image labels using Docker or Podman:

```bash
# Using Docker
docker inspect ghcr.io/gilmanlab/headjack:systemd --format='{{json .Config.Labels}}' | jq

# Using Podman
podman inspect ghcr.io/gilmanlab/headjack:systemd --format='{{json .Config.Labels}}' | jq

# Using Apple Container
container inspect ghcr.io/gilmanlab/headjack:systemd --format='{{json .Config.Labels}}' | jq
```

Example output:
Expand Down
8 changes: 4 additions & 4 deletions docs/docs/tutorials/custom-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This tutorial takes approximately 30-40 minutes to complete.
Before starting, ensure you have:

- Completed the [Getting Started](./getting-started) tutorial
- Docker, Podman, or Apple Container installed on your machine
- Docker or Podman installed on your machine
- Basic familiarity with Dockerfile syntax
- A project with specific runtime requirements (Python version, Node.js packages, system tools, etc.)

Expand Down Expand Up @@ -183,7 +183,7 @@ podman build -t my-app-headjack:latest -f Dockerfile.headjack .
```

:::note
Build with the same container runtime that Headjack uses. Check your configuration with `hjk config` and look for `runtime.name`. Images built with one runtime (Docker, Podman, or Apple Container) are not automatically available to others unless pushed to a registry.
Build with the same container runtime that Headjack uses. Check your configuration with `hjk config` and look for `runtime.name`. Images built with one runtime (Docker or Podman) are not automatically available to others unless pushed to a registry.
:::

The build takes several minutes as it compiles Python and Node.js. You will see output for each step:
Expand Down Expand Up @@ -282,7 +282,7 @@ hjk config default.base_image ghcr.io/your-org/my-app-headjack:latest

## Step 12: Build for Multiple Architectures

If your team uses both Intel and Apple Silicon Macs, build a multi-architecture image:
If your team uses both Intel and ARM Macs, build a multi-architecture image:

```bash
docker buildx build \
Expand All @@ -292,7 +292,7 @@ docker buildx build \
-f Dockerfile.headjack .
```

This creates an image that works on both architectures. Docker, Podman, and Apple Container automatically pull the correct variant.
This creates an image that works on both architectures. Docker and Podman automatically pull the correct variant.

## Complete Dockerfile

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/tutorials/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl';

Before starting, ensure you have:

- **macOS or Linux with a container runtime installed** - Headjack supports Docker (default), Podman, or Apple Container
- **macOS or Linux with a container runtime installed** - Headjack supports Docker (default) or Podman
- **Git installed** - Verify with `git --version`
- **A Claude Pro/Max subscription OR an Anthropic API key** - For Claude Code authentication
- **A git repository to work in** - Any project repository will work
Expand Down
4 changes: 2 additions & 2 deletions images/dind/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ ARG DOCKER_GPG_FINGERPRINT=9DC858229FC7DD38854AE2D88D81803C0EBFCD88
ARG USERNAME=developer

# =============================================================================
# iptables-legacy Workaround (ADR-002)
# Required for Docker-in-Docker in Apple Containerization Framework
# iptables-legacy Workaround
# Required for Docker-in-Docker in certain container environments
# =============================================================================

# hadolint ignore=DL3008
Expand Down
17 changes: 1 addition & 16 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,7 @@ func TestScripts(t *testing.T) {

// detectRuntime auto-detects the available container runtime.
func detectRuntime() string {
if runtime.GOOS == "darwin" {
// Prefer Apple Containerization on macOS
if _, err := exec.LookPath("container"); err == nil {
return "apple"
}
// Fall back to Docker
if _, err := exec.LookPath("docker"); err == nil {
return "docker"
}
}
// Linux: prefer Docker for CI consistency
// Prefer Docker for cross-platform consistency
if _, err := exec.LookPath("docker"); err == nil {
return "docker"
}
Expand Down Expand Up @@ -173,8 +163,6 @@ func evalCondition(cond string, runtimeName string) (bool, error) {
return runtimeName == "podman", nil
case "docker":
return runtimeName == "docker", nil
case "apple":
return runtimeName == "apple", nil
case "linux":
return runtime.GOOS == "linux", nil
case "darwin":
Expand All @@ -198,9 +186,6 @@ func cmdCleanupContainers(ts *testscript.TestScript, neg bool, args []string) {
var cmd *exec.Cmd

switch runtimeName {
case "apple":
// Apple container CLI cleanup
cmd = exec.Command("sh", "-c", `container list --format json 2>/dev/null | jq -r '.[].configuration.id // empty' | grep '^hjk-' | while read id; do container stop "$id" 2>/dev/null; container rm "$id" 2>/dev/null; done`)
case "docker":
cmd = exec.Command("sh", "-c", `docker ps -a --format '{{.Names}}' 2>/dev/null | grep '^hjk-' | xargs -r docker rm -f 2>/dev/null`)
default: // podman
Expand Down
16 changes: 2 additions & 14 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ import (
// baseDeps lists the external binaries that must always be available.
var baseDeps = []string{"git"}

// runtimeNameApple is the runtime name for Apple's container framework.
const runtimeNameApple = "apple"

// runtimeNameDocker is the runtime name for Docker.
const runtimeNameDocker = "docker"

Expand Down Expand Up @@ -129,13 +126,8 @@ func checkDependencies() error {

// getRuntimeBinary returns the binary name for the configured runtime.
func getRuntimeBinary() string {
if appConfig != nil {
switch appConfig.Runtime.Name {
case runtimeNameApple:
return "container"
case runtimeNameDocker:
return runtimeBinaryDocker
}
if appConfig != nil && appConfig.Runtime.Name != "" {
return appConfig.Runtime.Name // Runtime name matches binary name (docker, podman)
}
// Default to docker
return runtimeBinaryDocker
Expand Down Expand Up @@ -173,8 +165,6 @@ func initManager() error {
runtimeName = appConfig.Runtime.Name
}
switch runtimeName {
case runtimeNameApple:
runtime = container.NewAppleRuntime(executor, container.AppleConfig{})
case runtimeNameDocker:
runtime = container.NewDockerRuntime(executor, container.DockerConfig{})
default:
Expand Down Expand Up @@ -212,8 +202,6 @@ func initManager() error {
// runtimeNameToType converts a runtime name string to RuntimeType.
func runtimeNameToType(name string) instance.RuntimeType {
switch name {
case runtimeNameApple:
return instance.RuntimeApple
case runtimeNameDocker:
return instance.RuntimeDocker
default:
Expand Down
Loading
Loading