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
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Do NOT set both — OAuth token takes priority and API key will be ignored.
# ─────────────────────────────────────────────────────────────────────────

# ── Codex CLI (OpenAI) authentication ────────────────────────────────────
#
# Option 1: API key (recommended — bypasses interactive auth entirely)
# Create at: https://platform.openai.com/api-keys
# OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#
# Option 2: Device code flow (browser-free OAuth, no API key required)
# Run inside the container: codex auth login --auth device
# Follow the printed URL + code on your host browser.
# ─────────────────────────────────────────────────────────────────────────

# Project directory to mount (default: .. i.e. parent directory)
# PROJECT_DIR=/path/to/your/repo

Expand Down Expand Up @@ -50,6 +61,15 @@ CADDY_TLS=internal
# CADDY_BASICAUTH_USER=admin
# CADDY_BASICAUTH_HASH=$2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# ── Session memory ─────────────────────────────────────────────────────────
# Claude Code session memory (~/.claude) is persisted in a named Docker volume
# (claude-data) and survives container restarts automatically.
#
# CLAUDE.md template: on first run, if /workspace/CLAUDE.md does not exist,
# the entrypoint copies a template into it. Override the default by placing
# your own template at: <your-project>/.claude/CLAUDE.md.template
# ─────────────────────────────────────────────────────────────────────────────

# ── Egress firewall ──────────────────────────────────────────────────────────
# By default an iptables allowlist restricts outbound traffic to the known
# service endpoints (Claude, Copilot, GitHub CLI, npm). Requires the
Expand Down
1 change: 1 addition & 0 deletions .markdownlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ MD031: false # blanks-around-fences: style preference
MD032: false # blanks-around-lists: style preference
MD033: false # no-inline-html: used intentionally in some docs
MD034: false # no-bare-urls: URLs in technical docs are self-explanatory
MD060: false # table-column-style: compact tables are readable enough
18 changes: 18 additions & 0 deletions CLAUDE.md.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Project Instructions

You are working inside a clide container — a sandboxed development environment
with Claude Code, GitHub Copilot, GitHub CLI, and Codex pre-installed.

## Environment

- Workspace is mounted at `/workspace`
- You are running as user `clide` (uid 1001)
- Egress is restricted to an allowlist (see firewall.sh)
- Python venv is at `/opt/pyenv` (pytest and ruff pre-installed)
- Session memory persists across container restarts in `~/.claude/`

## Conventions

- Write clear, concise commit messages
- Run tests before committing when a test suite exists
- Prefer editing existing files over creating new ones
26 changes: 23 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ RUN ARCH="$(uname -m)" \
# hadolint ignore=DL3016
RUN npm install -g @anthropic-ai/claude-code

# Install Codex CLI (unpinned — tracks new features intentionally)
# hadolint ignore=DL3016,DL3059
RUN npm install -g @openai/codex

# Install Python 3 + dev tooling into an isolated venv
# - python3-venv provides the venv module (not always bundled in minimal images)
# - /opt/pyenv is world-readable so the unprivileged clide user can run tools
# - /opt/pyenv is owned by clide so the agent can pip-install workspace project
# dependencies on demand (e.g. pip install -r /workspace/clem/requirements.txt)
# without a container rebuild. pytest + ruff are pre-installed as a baseline.
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
Expand All @@ -59,22 +65,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PATH="/opt/pyenv/bin:${PATH}"

# Create unprivileged user and set up workspace
RUN useradd -m -s /bin/bash -u 1000 clide \
RUN useradd -m -s /bin/bash -u 1001 clide \
&& mkdir -p /workspace \
&& chown clide:clide /workspace
&& chown clide:clide /workspace \
# Hand venv ownership to clide so pip install works without sudo
&& chown -R clide:clide /opt/pyenv

# Add entrypoint scripts
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY claude-entrypoint.sh /usr/local/bin/claude-entrypoint.sh
COPY firewall.sh /usr/local/bin/firewall.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/claude-entrypoint.sh /usr/local/bin/firewall.sh

# Default CLAUDE.md template — seeded into /workspace on first run if none exists
COPY CLAUDE.md.template /usr/local/share/clide/CLAUDE.md.template

# tmux config — mouse support, sane splits, 256-colour
COPY --chown=clide:clide .tmux.conf /home/clide/.tmux.conf

# Switch to unprivileged user for user-scoped installs
USER clide

# Trust all directories for git operations.
# Clide is a single-user dev sandbox — volume-mounted repos from the host
# are often owned by a different UID (host user vs clide:1000), which causes
# git to refuse to operate and wastes tokens on `git config --global
# --add safe.directory ...` at the start of every session.
# Set safe.directory=* once at image build time to eliminate this entirely.
# See: https://git-scm.com/docs/git-config#Documentation/git-config.txt-safedirectory
RUN git config --global --add safe.directory '*'

# Install GitHub Copilot CLI (unpinned — tracks gh extension updates)
RUN curl -fsSL https://gh.io/copilot-install | bash

Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build rebuild web web-stop shell copilot gh claude help
.PHONY: build rebuild web web-stop shell copilot gh claude codex help

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
Expand Down Expand Up @@ -31,6 +31,9 @@ gh: ## Run GitHub CLI
claude: ## Run Claude Code CLI
CLAUDE_CODE_SIMPLE=1 docker compose run --rm claude

codex: ## Run Codex CLI (OpenAI)
docker compose run --rm codex

logs: ## Show web terminal logs
docker compose logs -f web

Expand Down
130 changes: 127 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@

Dockerized CLI toolkit with [GitHub Copilot CLI](https://github.com/github/copilot-cli), [GitHub CLI](https://cli.github.com/), and [Claude Code](https://www.anthropic.com/claude/code) — agentic terminal assistants in one container. Run against any local project without installing anything on your host. Access via terminal or browser-based web terminal.

## Architecture

```mermaid
graph TB
subgraph Host["Host Machine"]
workspace["📁 Project Directory\n(mounted as /workspace)"]
env[".env\n(secrets — gitignored)"]
browser["🌐 Browser\nlocalhost:7681"]
end

subgraph Container["clide Container (non-root: clide uid=1000)"]
direction TB
firewall["🔥 firewall.sh\n(iptables egress allowlist)"]

subgraph Services["Services"]
web["web\n(ttyd → tmux)"]
shell["shell\n(bash)"]
claude["claude\nClaude Code CLI"]
copilot["copilot\nGitHub Copilot CLI"]
gh["gh\nGitHub CLI"]
codex["codex\nCodex CLI"]
end
end

subgraph Internet["Internet (allowlisted endpoints only)"]
anthropic["api.anthropic.com\nClaude Code"]
ghcopilot["api.githubcopilot.com\nGitHub Copilot"]
github["api.github.com\ngithub.com\nGitHub CLI"]
npm["registry.npmjs.org\nnpm"]
end

browser -->|"HTTP (ttyd)"| web
workspace -->|"bind mount"| Container
env -->|"env vars"| Container
firewall --> Services
Services -->|"blocked by default"| firewall
firewall -->|"allowlisted"| Internet
```

> **Trust boundary:** the host trusts the container with a read-write mount of your project directory and your API credentials via `.env`. The container cannot reach the internet beyond the allowlisted endpoints (when `NET_ADMIN` is available). See [`SECURITY.md`](./SECURITY.md) for the full threat model.

## Prerequisites

- Docker + Docker Compose
Expand All @@ -18,6 +59,7 @@ Dockerized CLI toolkit with [GitHub Copilot CLI](https://github.com/github/copil
| GitHub Copilot CLI | `copilot` | `GH_TOKEN` |
| GitHub CLI | `gh` | `GH_TOKEN` |
| Claude Code | `claude` | `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY` |
| Codex CLI (OpenAI) | `codex` | `OPENAI_API_KEY` (or device code flow) |

### Claude Code authentication

Expand Down Expand Up @@ -50,6 +92,26 @@ ANTHROPIC_API_KEY=sk-ant-xxxxx
CLAUDE_CODE_SIMPLE=0 docker compose run --rm claude
```

### Codex CLI (OpenAI) authentication

Two auth methods are supported:

#### Option 1: API key (recommended)

Set `OPENAI_API_KEY` in `.env` to skip interactive auth entirely:
```env
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

#### Option 2: Device code flow (browser-free OAuth)

There is no browser inside the container, so the default OAuth redirect flow will not work.
Use device code flow instead — it prints a URL and code that you visit on your **host** browser:
```bash
codex auth login --auth device
# Open the printed URL in your host browser and enter the code
```

### tmux — multi-pane workflows

`tmux` is installed in the container and enabled by default in the **web terminal**. Every browser tab attaches to the same named session (`main`), so refreshing the page re-attaches rather than spawning a fresh shell.
Expand Down Expand Up @@ -107,6 +169,7 @@ CLIDE_TMUX=1
./clide shell # interactive shell with all CLIs
./clide copilot # run GitHub Copilot CLI
./clide claude # run Claude Code CLI
./clide codex # run Codex CLI (OpenAI)
./clide gh repo view # run GitHub CLI with args
./clide help # show all commands
```
Expand All @@ -117,6 +180,7 @@ make web # start web terminal
make shell # interactive shell
make copilot # run copilot
make claude # run Claude Code CLI
make codex # run Codex CLI (OpenAI)
make help # show all targets
```

Expand All @@ -143,6 +207,14 @@ Your project is mounted at `/workspace` inside the container.
### Bernard/Forge deployment
See [`DEPLOY.md`](./DEPLOY.md) for Caddy Docker Proxy integration. Uses `docker-compose.override.yml` (gitignored) for reverse proxy config that persists across git pulls.

## Additional docs

| Doc | Contents |
|---|---|
| [`SECURITY.md`](./SECURITY.md) | Threat model, trust boundaries, attack surface, hardening recommendations |
| [`RUNBOOK.md`](./RUNBOOK.md) | Operational runbook — health checks, logs, rebuilds, credential rotation, troubleshooting |
| [`DEPLOY.md`](./DEPLOY.md) | Production deployment with Caddy reverse proxy |

## Notes

- Tokens don't expire unless you set an expiry — set them once in `.env` and you're done. OAuth tokens from `claude setup-token` are valid for 1 year.
Expand All @@ -159,6 +231,45 @@ See [`DEPLOY.md`](./DEPLOY.md) for Caddy Docker Proxy integration. Uses `docker-
make claude
```

## Compatibility

### Host OS

| OS | Status | Notes |
|---|---|---|
| Linux | ✅ Supported | Native Docker — full functionality including egress firewall |
| macOS (Apple Silicon) | ✅ Supported | Docker Desktop required; `arm64` image builds natively |
| macOS (Intel) | ✅ Supported | Docker Desktop required |
| Windows (WSL2) | ✅ Supported | Docker Desktop with WSL2 backend required |
| Windows (no WSL2) | ⚠️ Partial | Egress firewall requires `NET_ADMIN`; availability varies by runtime |

### Docker

| Requirement | Minimum |
|---|---|
| Docker Engine | 20.10+ |
| Docker Compose | v2.0+ (`docker compose`, not `docker-compose`) |

### CPU architecture

| Arch | Status |
|---|---|
| `amd64` (x86_64) | ✅ Supported |
| `arm64` (Apple Silicon, Graviton) | ✅ Supported |

### Web terminal browser support

| Browser | Status |
|---|---|
| Chrome / Chromium | ✅ Supported |
| Firefox | ✅ Supported |
| Safari | ✅ Supported |
| Edge | ✅ Supported |

### Egress firewall support

The `iptables` egress firewall requires the `NET_ADMIN` capability and a Linux kernel with `iptables` support. It works out of the box on Linux hosts and Docker Desktop (macOS/Windows). If unavailable, the firewall degrades gracefully — a warning is printed and egress is unrestricted.

## Egress firewall

By default every clide container applies an **iptables egress allowlist** at startup, restricting outbound traffic to the known service endpoints. All bundled CLIs continue to work normally within these defaults.
Expand All @@ -172,6 +283,8 @@ By default every clide container applies an **iptables egress allowlist** at sta
| `api.github.com` | GitHub Copilot CLI · GitHub CLI |
| `github.com` | GitHub CLI |
| `registry.npmjs.org` | npm package updates |
| `api.openai.com` | Codex CLI |
| `auth.openai.com` | Codex CLI — device code auth |

DNS (port 53) and loopback traffic are always allowed.

Expand All @@ -197,17 +310,28 @@ The firewall uses `iptables` and requires the `NET_ADMIN` capability, which is a

## Python dev tooling

The container includes a Python 3 virtual environment at `/opt/pyenv` with the following tools pre-installed and on `PATH`:
The container includes a Python 3 **virtualenv** at `/opt/pyenv` (note: this is a plain `python3 -m venv`, not the pyenv version manager) with the following tools pre-installed and on `PATH`:

| Tool | Purpose |
|------|---------|
| `pytest` | Test runner — `pytest tests/` |
| `ruff` | Linter + formatter — `ruff check .` / `ruff format .` |

To install project dependencies inside the container:
The venv is **clide-owned** — `pip install` works without `sudo` or a container rebuild. To install workspace project dependencies:

```bash
pip install -r requirements.txt
pip install -r /workspace/<repo>/requirements.txt
```

The venv is at `/opt/pyenv`; `pip`, `pytest`, and `ruff` are all directly callable without activation.

> **Note:** `pip install` requires outbound access to PyPI. Add these to `CLIDE_ALLOWED_HOSTS` if the egress firewall is enabled:
> ```env
> CLIDE_ALLOWED_HOSTS=pypi.org,files.pythonhosted.org
> ```

## Git workspace repos

`safe.directory = *` is pre-configured in the clide user's gitconfig at image build time. Volume-mounted repos cloned from GitHub are often owned by the host UID rather than `clide:1000`, which normally causes git to refuse to operate with a "detected dubious ownership" error. This is eliminated entirely — `git status`, `git log`, and all other git operations work from the first prompt without any `git config` boilerplate.

> **Security note:** `safe.directory = *` trusts all directories unconditionally. This is appropriate for a single-user dev sandbox where you control what gets mounted, but it means git will operate in any directory regardless of ownership. If you share the container or mount untrusted paths, consider replacing `*` with the specific paths you use (e.g. `/workspace`).
Loading