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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to aidc are tracked here. Format follows [Keep a Changelog](

### Added

- **Grok Build agent.** `aidc grok` launches xAI's Grok Build CLI inside the container, with a `grok_home` named volume (`~/.grok`), a read-only `/host-seed/grok` seed mount, `sync_grok` seeding in `bootstrap-state.sh`, and `aidc sync-config`/`sync-sessions` support. Grok reads the same generated `AGENTS.md`.
- **GPL-3.0 license.** `LICENSE` at repo root.
- **Vulnerability disclosure policy.** `SECURITY.md` documents scope, reporting channels, and timelines.
- **Shellcheck CI.** `.github/workflows/shellcheck.yml` lints `bin/aidc`, `install.sh`, `lib/aidc.sh`, and `templates/**/*.sh`, plus syntax-checks the Python clipboard server.
Expand All @@ -22,6 +23,8 @@ All notable changes to aidc are tracked here. Format follows [Keep a Changelog](

### Changed

- **Coding agents now install as native prebuilt binaries.** `claude`, `codex`, and `opencode` switched from a root `npm install -g` to each vendor's native `curl | sh` installer (into `~/.local/bin`), dropping the Node runtime dependency for the agents and shrinking the npm supply-chain surface. Removes the only build step that ran package-manager installs *before* `pmg setup install`.
- **pmg wired in before any user-level install.** `pmg setup install` is now the first `USER vscode` build step. Interception is documented as riding on the `~/.pmg/bin` PATH shims (first on `ENV PATH`) rather than the rc-file aliases pmg also writes — build `RUN` steps and exec'd agents never source rc files. `docs/security.md` now describes the shared-credentials seed-mount/env-passthrough matrix.
- **Pinned `gitleaks` and `vet` versions** in `templates/devcontainer/Dockerfile.tmpl` (`v8.30.1` and `v1.17.3` respectively). Defaults were `latest`, which resolved at build time and defeated the surrounding base-image SHA pin. Repin instructions are in the Dockerfile.
- **README** and the docs/ tree brought into sync with the above. README's command list includes `aidc status`, `aidc down`, `aidc destroy`, `aidc exec`, and `aidc sync-sessions`. `docs/security.md` covers the scanners, supply-chain guardrails, and egress firewall; `docs/claude-profiles.md` covers the local-model profiles (`localhost.env.example`, `localnetwork.env.example`).

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Status: alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#status)
[![macOS only](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](#prereqs)

**aidc** — short for **AI Dev Container** — is a one-command devcontainer wrapper for AI coding agents (`claude`, `codex`, `opencode`, `cursor-agent`). It scaffolds a hardened Linux container per repo, mounts your code at `/workspace`, persists agent state in named Docker volumes (so agents don't read your `~/.ssh` or your shell history), and bakes in always-on security scanners and supply-chain guardrails.
**aidc** — short for **AI Dev Container** — is a one-command devcontainer wrapper for AI coding agents (`claude`, `codex`, `opencode`, `grok`, `cursor-agent`). It scaffolds a hardened Linux container per repo, mounts your code at `/workspace`, persists agent state in named Docker volumes (so agents don't read your `~/.ssh` or your shell history), and bakes in always-on security scanners and supply-chain guardrails.

If you're already running these agents directly on your Mac and have been quietly nervous about it, this is for you.

Expand Down Expand Up @@ -37,12 +37,13 @@ aidc init # one-time scaffold; writes .devcontainer/, .ai-container/, C
aidc claude # auto-runs `aidc up` if needed, then drops you into Claude Code in the container
```

Tool commands (`aidc claude` / `codex` / `opencode` / `cursor-agent`) auto-bootstrap the container on first run.
Tool commands (`aidc claude` / `codex` / `opencode` / `grok` / `cursor-agent`) auto-bootstrap the container on first run.

## What aidc actually does

- creates local-only `.devcontainer/`, `.ai-container/`, `CLAUDE.md`, `AGENTS.md`, and `.cursor/rules/00-core-logics.mdc`
- mounts project code only at `/workspace`; overlays `/workspace/.devcontainer` read-only inside the container
- installs the coding agents (`claude`, `codex`, `opencode`, `grok`) as native prebuilt binaries — no npm-global, no Node runtime dependency for the agents themselves
- persists tool state in per-repo Docker volumes instead of mounting whole host homes
- seeds selected config from host read-only mounts on first startup
- creates one `CORE_LOGICS` git worktree per repo and mounts it at `/opt/CORE_LOGICS` for shared cross-repo notes
Expand Down Expand Up @@ -75,11 +76,12 @@ aidc exec -- <command>...
aidc claude [--profile NAME] [--provider NAME] [--list-profiles] [-- ...]
aidc codex [-- ...]
aidc opencode [-- ...]
aidc grok [-- ...]
aidc cursor-agent [-- ...]
aidc cursor
aidc sync-claude-aliases
aidc sync-config <claude|codex|opencode|all>
aidc sync-sessions [claude|codex|opencode|all]
aidc sync-config <claude|codex|opencode|grok|all>
aidc sync-sessions [claude|codex|opencode|grok|all]
```

`aidc status` shows the container + mounts/config for the current folder. `--global` lists every aidc container on the host with disk/CPU/memory and a totals line.
Expand Down
6 changes: 4 additions & 2 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ aidc up # build + start container
| `aidc claude --profile <name>` | start Claude against a host-defined profile |
| `aidc codex` | start OpenAI Codex |
| `aidc opencode` | start OpenCode |
| `aidc grok` | start Grok Build |
| `aidc cursor-agent` | start Cursor Agent |
| `aidc cursor` | open host Cursor on the repo |
| `aidc status` | container + config/mounts status for this folder |
Expand All @@ -53,8 +54,9 @@ aidc up # build + start container
/home/vscode/.claude Claude state (named volume)
/home/vscode/.codex Codex state (named volume)
/home/vscode/.config/opencode OpenCode state (named volume)
/home/vscode/.grok Grok state (named volume)
/commandhistory bash + zsh history (named volume)
/host-seed/{claude,codex,opencode,gitconfig} read-only host seeds
/host-seed/{claude,codex,opencode,grok,gitconfig} read-only host seeds
```

`GIT_CONFIG_GLOBAL=/home/vscode/.gitconfig.local` — host gitconfig is seed-only, in-container `git config --global` writes land in the overlay (ephemeral across rebuilds).
Expand Down Expand Up @@ -100,7 +102,7 @@ sudo apt-get install -y --no-install-recommends golang-go
go install golang.org/x/tools/gopls@latest
```

Runs as `vscode` at image build time, with passwordless `sudo` available for system packages. The `COPY` is the last layer in the Dockerfile, so edits invalidate **only** the project-setup layer — the heavy base layers (apt, uv/Python, npm globals, pmg/vet/gryph/rtk) stay cached.
Runs as `vscode` at image build time, with passwordless `sudo` available for system packages. The `COPY` is the last layer in the Dockerfile, so edits invalidate **only** the project-setup layer — the heavy base layers (apt, uv/Python, native agent binaries, pmg/vet/gryph/rtk) stay cached.

After editing, `aidc rebuild` (or just `aidc up` — `--build` is implicit) picks up the change.

Expand Down
37 changes: 35 additions & 2 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,45 @@ The scaffold writes a "Security guardrails (non-negotiable)" block into `CLAUDE.

## Supply-chain guardrails (always on)

The container ships with SafeDep's [`pmg`](https://github.com/safedep/pmg) and [`vet`](https://github.com/safedep/vet) baked in. `pmg setup install` runs at image build, so `~/.pmg/bin` is first on `PATH` and intercepts `npm`, `pnpm`, `yarn`, `bun`, `npx`, `pnpx`, `pip`, `pip3`, `uv`, and `poetry` — including subprocess calls from agents (Claude, Codex, OpenCode, Cursor Agent). Malicious packages are blocked before install.
The container ships with SafeDep's [`pmg`](https://github.com/safedep/pmg) and [`vet`](https://github.com/safedep/vet) baked in. `pmg setup install` runs at image build **before any user-level package install**, and interception rides on the `~/.pmg/bin` PATH shims — which are first on the image `ENV PATH` for both build and runtime. It is deliberately **not** dependent on the shell aliases pmg also writes to `~/.zshrc`/`~/.bashrc`: Docker build `RUN` steps and exec'd agent subprocesses never source rc files, so only the PATH shims reliably gate package managers. The shims intercept `npm`, `pnpm`, `yarn`, `bun`, `npx`, `pnpx`, `pip`, `pip3`, `uv`, and `poetry` — including subprocess calls from agents (Claude, Codex, OpenCode, Grok, Cursor Agent). Malicious packages are blocked before install.

Run scans by hand with `aidc exec -- vet scan -D /workspace`. Re-run `pmg setup doctor` inside the container to verify wiring (`aidc exec -- pmg setup doctor`).
The coding agents themselves are installed as **native prebuilt binaries** (via each vendor's `curl | sh` installer), not `npm install -g`, so there is no agent-install step for pmg to vet and no Node runtime dependency for the agents. The `NPM_CONFIG_*` hardening in the image still governs any npm the agents or project toolchains invoke at runtime, which the pmg shims gate.

Run scans by hand with `aidc exec -- vet scan -D /workspace`. Re-run `pmg setup doctor` inside the container to verify wiring (`aidc exec -- pmg setup doctor`). To confirm interception is alias-independent, check that the shim wins without sourcing rc files: `aidc exec -- bash -c 'command -v npm'` should resolve under `~/.pmg/bin`.

If the egress firewall is enabled, the allowlist already includes `api.safedep.io`, `vetpkg.dev`, `osv.dev`, and `semgrep.dev`.

## Sharing credentials with the agents

aidc never mounts your whole host home into the container. Instead each agent's
config/auth is shared two ways, both scoped to that agent:

**1. Read-only seed mounts (the default path).** On first startup
`bootstrap-state.sh init` copies selected files from host config dirs (mounted
read-only at `/host-seed/<tool>`) into the agent's per-repo volume:

| Agent | Host source | Container volume | Seeded by |
|---|---|---|---|
| Claude | `~/.claude` | `~/.claude` | `settings.json`, `CLAUDE.md` |
| Codex | `~/.codex` | `~/.codex` | `auth.json`, `config.toml`, `AGENTS.md`, `rules/`, `skills/` |
| OpenCode | `~/.config/opencode` | `~/.config/opencode` | `opencode.json`, `plugins/` |
| Grok | `~/.grok` | `~/.grok` | `config.toml` / `user-settings.json` / `auth.json` (whichever exists) |

Re-sync after changing host config with `aidc sync-config <claude|codex|opencode|grok|all>`.
Because the seed is read-only and only specific files are copied, the agents
reuse your existing logins without the container being able to write back to the
host. After interactive login *inside* the container, credentials persist in the
named volume across restarts (and are wiped by `aidc destroy`).

**2. Environment-variable passthrough.** For headless/API-key auth, `aidc`
forwards a fixed set of host env vars into the agent process when present:
`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `OPENAI_API_KEY`,
`CURSOR_API_KEY`, `OPENROUTER_API_KEY`, `OPENAI_BASE_URL`. OpenCode and Grok can
both speak to multiple providers, so these shared keys let all the agents reuse
the same host credentials. For xAI specifically, either log in interactively
(persisted in `~/.grok`) or export the xAI key on the host and add it to the
passthrough list in `AIDC_PASSTHROUGH_ENV_KEYS` (`lib/aidc.sh`) before launching.

## Agent guardrails: gryph + rtk

The image also ships [`gryph`](https://github.com/safedep/gryph) (SafeDep's agent security layer — patches hook entries into the detected agents' settings) and [`rtk`](https://github.com/rtk-ai/rtk) (Rust Token Killer — token-saving CLI proxy that rewrites commands like `git status` → `rtk git status` via the Claude Code hook).
Expand Down
34 changes: 28 additions & 6 deletions lib/aidc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ aidc::main() {
opencode)
aidc::cmd_opencode "$@"
;;
grok)
aidc::cmd_grok "$@"
;;
cursor-agent)
aidc::cmd_cursor_agent "$@"
;;
Expand Down Expand Up @@ -121,11 +124,12 @@ Usage:
aidc claude [--profile NAME] [--provider NAME] [--list-profiles] [-- ...]
aidc codex [-- ...]
aidc opencode [-- ...]
aidc grok [-- ...]
aidc cursor-agent [-- ...]
aidc cursor
aidc sync-claude-aliases
aidc sync-config <claude|codex|opencode|all>
aidc sync-sessions [claude|codex|opencode|all]
aidc sync-config <claude|codex|opencode|grok|all>
aidc sync-sessions [claude|codex|opencode|grok|all]

Notes:
- Run commands from the repo root you want to isolate.
Expand Down Expand Up @@ -385,6 +389,7 @@ aidc::status_config() {
aidc::status_mount_row "/host-seed/claude" "$AIDC_HOST_SEED_CLAUDE" "$c_dim" "$c_rst"
aidc::status_mount_row "/host-seed/codex" "$AIDC_HOST_SEED_CODEX" "$c_dim" "$c_rst"
aidc::status_mount_row "/host-seed/opencode" "$AIDC_HOST_SEED_OPENCODE" "$c_dim" "$c_rst"
aidc::status_mount_row "/host-seed/grok" "$AIDC_HOST_SEED_GROK" "$c_dim" "$c_rst"
aidc::status_mount_row "/host-seed/gitconfig" "$AIDC_GITCONFIG_SOURCE" "$c_dim" "$c_rst"
aidc::status_mount_row "/host-clipboard" "$AIDC_CLIPBOARD_DIR_SOURCE" "$c_dim" "$c_rst"

Expand Down Expand Up @@ -689,6 +694,10 @@ aidc::cmd_opencode() {
aidc::run_tool "opencode" "" "$@"
}

aidc::cmd_grok() {
aidc::run_tool "grok" "" "$@"
}

aidc::cmd_cursor_agent() {
aidc::run_tool "cursor-agent" "" "$@"
}
Expand All @@ -713,7 +722,7 @@ aidc::cmd_sync_config() {
local workspace
workspace="$(aidc::default_workspace)"
local tool="${1:-}"
[[ -n "$tool" ]] || aidc::die "usage: aidc sync-config <claude|codex|opencode|all>"
[[ -n "$tool" ]] || aidc::die "usage: aidc sync-config <claude|codex|opencode|grok|all>"
aidc::ensure_container_running "$workspace"
aidc::compose "$workspace" exec workspace /workspace/.devcontainer/scripts/bootstrap-state.sh sync "$tool"
aidc::log "synced $tool config into the container volume"
Expand All @@ -726,14 +735,15 @@ aidc::cmd_sync_sessions() {
aidc::ensure_container_running "$workspace"

case "$tool" in
claude|codex|opencode|all) ;;
*) aidc::die "usage: aidc sync-sessions [claude|codex|opencode|all]" ;;
claude|codex|opencode|grok|all) ;;
*) aidc::die "usage: aidc sync-sessions [claude|codex|opencode|grok|all]" ;;
esac

if [[ "$tool" == "all" ]]; then
aidc::sync_session_tool "$workspace" claude
aidc::sync_session_tool "$workspace" codex
aidc::sync_session_tool "$workspace" opencode
aidc::sync_session_tool "$workspace" grok
else
aidc::sync_session_tool "$workspace" "$tool"
fi
Expand All @@ -757,6 +767,10 @@ aidc::sync_session_tool() {
container_src="/home/vscode/.config/opencode/projects"
host_dst="$HOME/.config/opencode/projects"
;;
grok)
container_src="/home/vscode/.grok/sessions"
host_dst="$HOME/.grok/sessions"
;;
*)
aidc::die "unknown session tool: $tool"
;;
Expand Down Expand Up @@ -804,6 +818,12 @@ aidc::run_tool() {
opencode)
command=("opencode")
;;
grok)
# The container is already the isolation boundary; grok runs unsandboxed
# like the other agents. Grok Build has operating modes (e.g. plan/auto);
# append the full-autonomy mode flag here once confirmed against the CLI.
command=("grok")
;;
cursor-agent)
command=("cursor-agent" "--sandbox" "disabled" "-f")
;;
Expand Down Expand Up @@ -1510,7 +1530,7 @@ aidc::ensure_core_worktree() {

aidc::ensure_host_config_dirs() {
mkdir -p "$AIDC_HOST_CONFIG_ROOT" "$AIDC_EMPTY_ROOT" "$AIDC_CLAUDE_PROFILE_ROOT"
mkdir -p "$AIDC_EMPTY_ROOT/claude" "$AIDC_EMPTY_ROOT/codex" "$AIDC_EMPTY_ROOT/opencode" "$AIDC_EMPTY_ROOT/clipboard"
mkdir -p "$AIDC_EMPTY_ROOT/claude" "$AIDC_EMPTY_ROOT/codex" "$AIDC_EMPTY_ROOT/opencode" "$AIDC_EMPTY_ROOT/grok" "$AIDC_EMPTY_ROOT/clipboard"
touch "$AIDC_EMPTY_ROOT/gitconfig"
}

Expand Down Expand Up @@ -1609,6 +1629,8 @@ aidc::export_compose_env() {
AIDC_HOST_SEED_CODEX="$(aidc::mount_dir_or_empty "$HOME/.codex" "codex")"
export AIDC_HOST_SEED_OPENCODE
AIDC_HOST_SEED_OPENCODE="$(aidc::mount_dir_or_empty "$HOME/.config/opencode" "opencode")"
export AIDC_HOST_SEED_GROK
AIDC_HOST_SEED_GROK="$(aidc::mount_dir_or_empty "$HOME/.grok" "grok")"
export AIDC_GITCONFIG_SOURCE
AIDC_GITCONFIG_SOURCE="$(aidc::mount_file_or_empty "$HOME/.gitconfig" "gitconfig")"
# Host-clipboard bridge is opt-in (off by default). When disabled, mount an
Expand Down
26 changes: 22 additions & 4 deletions templates/devcontainer/Dockerfile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ ENV NPM_CONFIG_IGNORE_SCRIPTS=true \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
NODE_OPTIONS=--max-old-space-size=4096

RUN npm_config_ignore_scripts=false npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai@latest \
&& npm cache clean --force
# Coding agents are installed as native prebuilt binaries per-user below (no
# npm-global, no Node runtime dependency for the agents themselves). The
# NPM_CONFIG_* hardening above still governs any npm the agents or project
# toolchains invoke at runtime.

# Supply-chain guardrails: pmg intercepts npm/pip/uv/etc. and runs vet checks
# before installs. Binaries installed system-wide; shell/shim wiring done
Expand Down Expand Up @@ -187,18 +189,34 @@ RUN chmod 0755 /usr/local/bin/aidc-bootstrap-claude
USER vscode

ENV HOME=/home/vscode
# ~/.pmg/bin is FIRST on PATH on purpose. pmg interception rides on these PATH
# shims, NOT on the shell aliases pmg also writes to ~/.zshrc/~/.bashrc: build
# RUN steps and exec'd agent subprocesses never source rc files, so only the
# shims (first on PATH at build and runtime) reliably gate npm/pip/uv/etc.
ENV PATH=/home/vscode/.pmg/bin:/home/vscode/.local/bin:/usr/local/bin:/usr/bin:/bin

# Wire pmg in before ANY user-level package install so nothing slips past it.
RUN pmg setup install \
&& pmg setup doctor || true

# Coding agents as native prebuilt binaries. Each installer drops a binary into
# ~/.local/bin (already on PATH) via plain curl — no npm dependency resolution,
# so pmg has nothing to vet here; the "pmg first" invariant above still holds.
# Grok's binary is forced into ~/.local/bin via GROK_BIN_DIR so the grok_home
# volume mounted at ~/.grok at runtime cannot shadow it.
RUN mkdir -p \
/home/vscode/.local/bin \
/home/vscode/.claude \
/home/vscode/.codex \
/home/vscode/.config/opencode \
/home/vscode/.cursor-agent \
/home/vscode/.grok \
&& curl -fsSL https://claude.ai/install.sh | bash \
&& curl -fsSL https://chatgpt.com/codex/install.sh | sh \
&& curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path \
&& curl -fsS https://cursor.com/install | bash \
&& { cursor-agent --version || true; } \
&& pmg setup install \
&& pmg setup doctor || true
&& curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR=/home/vscode/.local/bin bash

# Auto-detected toolchains. aidc fills AIDC_TOOLCHAINS by scanning the
# workspace for marker files (go.mod, Cargo.toml, etc.). Override per repo
Expand Down
8 changes: 8 additions & 0 deletions templates/devcontainer/compose.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ services:
source: ${AIDC_HOST_SEED_OPENCODE}
target: /host-seed/opencode
read_only: true
- type: bind
source: ${AIDC_HOST_SEED_GROK}
target: /host-seed/grok
read_only: true
- type: bind
source: ${AIDC_GITCONFIG_SOURCE}
target: /host-seed/gitconfig
Expand All @@ -72,10 +76,14 @@ services:
- type: volume
source: cursor_agent_home
target: /home/vscode/.cursor-agent
- type: volume
source: grok_home
target: /home/vscode/.grok

volumes:
command_history:
claude_home:
codex_home:
opencode_home:
cursor_agent_home:
grok_home:
Loading
Loading