mirror of
https://github.com/trailofbits/claude-code-devcontainer
synced 2026-06-21 14:11:48 +00:00
Add Codex support to devcontainer
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# shellcheck shell=bash
|
||||
# Zsh configuration for Claude Code devcontainer
|
||||
# Zsh configuration for AI coding agent devcontainer
|
||||
|
||||
# Add Claude Code to PATH
|
||||
# Add user-installed agent CLIs to PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# fnm (Fast Node Manager)
|
||||
@@ -33,6 +33,7 @@ setopt ALWAYS_TO_END # Move cursor to end after completion
|
||||
alias fd=fdfind
|
||||
alias sg=ast-grep
|
||||
alias claude-yolo='claude --dangerously-skip-permissions'
|
||||
alias codex-yolo='codex --dangerously-bypass-approvals-and-sandbox'
|
||||
alias ll='ls -lah --color=auto'
|
||||
alias la='ls -A --color=auto'
|
||||
alias l='ls -CF --color=auto'
|
||||
|
||||
+9
-5
@@ -1,4 +1,4 @@
|
||||
# Claude Code Devcontainer
|
||||
# AI Coding Agent Devcontainer
|
||||
# Based on Microsoft devcontainer image for better devcontainer integration
|
||||
FROM ghcr.io/astral-sh/uv:0.10@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82 AS uv
|
||||
FROM mcr.microsoft.com/devcontainers/base:ubuntu24.04@sha256:4bcb1b466771b1ba1ea110e2a27daea2f6093f9527fb75ee59703ec89b5561cb
|
||||
@@ -10,7 +10,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Install additional system packages (base image already includes git, curl, sudo, etc.)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Sandboxing support for Claude Code
|
||||
# Sandboxing support for agent CLIs
|
||||
bubblewrap \
|
||||
socat \
|
||||
# Modern CLI tools
|
||||
@@ -55,10 +55,10 @@ RUN ARCH=$(dpkg --print-architecture) && \
|
||||
curl -fsSL "https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/fzf-${FZF_VERSION}-${FZF_ARCH}.tar.gz" | tar -xz -C /usr/local/bin
|
||||
|
||||
# Create directories and set ownership (combined for fewer layers)
|
||||
RUN mkdir -p /commandhistory /workspace /home/vscode/.claude /opt && \
|
||||
RUN mkdir -p /commandhistory /workspace /home/vscode/.claude /home/vscode/.codex /opt && \
|
||||
touch /commandhistory/.bash_history && \
|
||||
touch /commandhistory/.zsh_history && \
|
||||
chown -R vscode:vscode /commandhistory /workspace /home/vscode/.claude /opt
|
||||
chown -R vscode:vscode /commandhistory /workspace /home/vscode/.claude /home/vscode/.codex /opt
|
||||
|
||||
# Set environment variables
|
||||
ENV DEVCONTAINER=true
|
||||
@@ -71,7 +71,7 @@ WORKDIR /workspace
|
||||
# Switch to non-root user for remaining setup
|
||||
USER vscode
|
||||
|
||||
# Set PATH early so claude and other user-installed binaries are available
|
||||
# Set PATH early so agent CLIs and other user-installed binaries are available
|
||||
ENV PATH="/home/vscode/.local/bin:$PATH"
|
||||
|
||||
# Install Claude Code natively with marketplace plugins
|
||||
@@ -80,6 +80,10 @@ RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
||||
claude plugin marketplace add trailofbits/skills && \
|
||||
claude plugin marketplace add trailofbits/skills-curated
|
||||
|
||||
# Install OpenAI Codex CLI with Trail of Bits marketplace
|
||||
RUN curl -fsSL https://chatgpt.com/codex/install.sh | sh && \
|
||||
codex plugin marketplace add trailofbits/skills
|
||||
|
||||
# Install Python 3.13 via uv (fast binary download, not source compilation)
|
||||
RUN uv python install 3.13 --default
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Claude Code in a devcontainer
|
||||
# Claude Code and Codex in a devcontainer
|
||||
|
||||
A sandboxed development environment for running Claude Code with `bypassPermissions` safely enabled. Built at [Trail of Bits](https://www.trailofbits.com/) for security audit workflows.
|
||||
A sandboxed development environment for running Claude Code and OpenAI Codex CLI with their low-friction agent modes enabled. Built at [Trail of Bits](https://www.trailofbits.com/) for security audit workflows.
|
||||
|
||||
## Why Use This?
|
||||
|
||||
Running Claude with `bypassPermissions` on your host machine is risky—it can execute any command without confirmation. This devcontainer provides **filesystem isolation** so you get the productivity benefits of unrestricted Claude without risking your host system.
|
||||
Running coding agents with unrestricted command execution on your host machine is risky. Claude Code `bypassPermissions` and Codex `danger-full-access` can execute commands, install packages, and modify files without stopping for confirmation. This devcontainer provides **filesystem isolation** so the agent can work freely inside `/workspace` without getting broad access to your host.
|
||||
|
||||
**Designed for:**
|
||||
|
||||
- **Security audits**: Review client code without risking your host
|
||||
- **Untrusted repositories**: Explore unknown codebases safely
|
||||
- **Experimental work**: Let Claude modify code freely in isolation
|
||||
- **Experimental work**: Let agents modify code freely in isolation
|
||||
- **Multi-repo engagements**: Work on multiple related repositories
|
||||
|
||||
## Prerequisites
|
||||
@@ -74,6 +74,10 @@ git clone <untrusted-repo>
|
||||
cd untrusted-repo
|
||||
devc . # Installs template + starts container
|
||||
devc shell # Opens shell in container
|
||||
|
||||
# Inside container:
|
||||
claude # Claude Code
|
||||
codex # OpenAI Codex CLI
|
||||
```
|
||||
|
||||
**VS Code / Cursor:**
|
||||
@@ -111,10 +115,14 @@ devc shell # Opens shell in container
|
||||
git clone <client-repo-1>
|
||||
git clone <client-repo-2>
|
||||
cd client-repo-1
|
||||
claude # Ready to work
|
||||
codex # or: claude
|
||||
```
|
||||
|
||||
## Token-Based Auth (Headless)
|
||||
## Authentication
|
||||
|
||||
### Claude Code
|
||||
|
||||
Interactive Claude login works normally inside the container.
|
||||
|
||||
For headless servers or to skip the interactive login wizard:
|
||||
|
||||
@@ -128,45 +136,66 @@ The token is forwarded into the container. On each container creation, `post_ins
|
||||
|
||||
This works around Claude Code's interactive onboarding wizard always showing in containers, even with valid credentials ([#8938](https://github.com/anthropics/claude-code/issues/8938)).
|
||||
|
||||
If you don't set a token, the interactive login flow works as before.
|
||||
### Codex
|
||||
|
||||
Interactive Codex login works normally inside the container:
|
||||
|
||||
```bash
|
||||
codex
|
||||
```
|
||||
|
||||
Codex will prompt you to sign in with ChatGPT or an API key. Its auth and config are stored in the persistent `~/.codex` volume.
|
||||
|
||||
The container also adds the Trail of Bits Codex plugin marketplace with `codex plugin marketplace add trailofbits/skills`.
|
||||
|
||||
For headless use, export one of these on the host before creating or rebuilding the container:
|
||||
|
||||
```bash
|
||||
export CODEX_ACCESS_TOKEN=<codex-access-token>
|
||||
# or
|
||||
export OPENAI_API_KEY=<openai-api-key>
|
||||
devc rebuild
|
||||
```
|
||||
|
||||
If both are set, `CODEX_ACCESS_TOKEN` wins. `post_install.py` persists the credential with `codex login --with-access-token` or `codex login --with-api-key`.
|
||||
|
||||
## CLI Helper Commands
|
||||
|
||||
```
|
||||
devc . Install template + start container in current directory
|
||||
devc up Start the devcontainer
|
||||
devc rebuild Rebuild container (preserves persistent volumes)
|
||||
devc destroy [-f] Remove container, volumes, and image for current project
|
||||
devc down Stop the container
|
||||
devc shell Open zsh shell in container
|
||||
devc exec CMD Execute command inside the container
|
||||
devc upgrade Upgrade Claude Code in the container
|
||||
devc mount SRC DST Add a bind mount (host → container)
|
||||
devc sync [NAME] Sync Claude Code sessions from devcontainers to host
|
||||
devc template DIR Copy devcontainer files to directory
|
||||
devc self-install Install devc to ~/.local/bin
|
||||
devc . Install template + start container in current directory
|
||||
devc up Start the devcontainer
|
||||
devc rebuild Rebuild container (preserves persistent volumes)
|
||||
devc destroy [-f] Remove container, volumes, and image for current project
|
||||
devc down Stop the container
|
||||
devc shell Open zsh shell in container
|
||||
devc exec CMD Execute command inside the container
|
||||
devc upgrade [claude|codex|all] Upgrade agent CLI(s), defaults to all
|
||||
devc mount SRC DST Add a bind mount (host -> container)
|
||||
devc sync [NAME] Sync Claude Code sessions from devcontainers to host
|
||||
devc template DIR Copy devcontainer files to directory
|
||||
devc self-install Install devc to ~/.local/bin
|
||||
```
|
||||
|
||||
> **Note:** Use `devc destroy` to clean up a project's Docker resources. Removing containers manually (e.g., `docker rm`) will leave orphaned volumes and images behind that `devc destroy` won't be able to find.
|
||||
|
||||
## Session Sync for `/insights`
|
||||
## Claude Session Sync for `/insights`
|
||||
|
||||
Claude Code's `/insights` command analyzes your session history, but it only reads from `~/.claude/projects/` on the host. Sessions inside devcontainer volumes are invisible to it.
|
||||
|
||||
`devc sync` copies session logs from all devcontainers (running and stopped) to the host so `/insights` can include them:
|
||||
`devc sync` copies Claude session logs from all devcontainers (running and stopped) to the host so `/insights` can include them:
|
||||
|
||||
```bash
|
||||
devc sync # Sync all devcontainers
|
||||
devc sync crypto # Filter by project name (substring match)
|
||||
```
|
||||
|
||||
Devcontainers are auto-discovered via Docker labels — no need to know container names or IDs. The sync is incremental, so it's safe to run repeatedly.
|
||||
Devcontainers are auto-discovered via Docker labels. The sync is incremental, so it's safe to run repeatedly.
|
||||
|
||||
## File Sharing
|
||||
|
||||
### VS Code / Cursor
|
||||
|
||||
Drag files from your host into the VS Code Explorer panel — they are copied into `/workspace/` automatically. No configuration needed.
|
||||
Drag files from your host into the VS Code Explorer panel. They are copied into `/workspace/` automatically.
|
||||
|
||||
### Terminal: `devc mount`
|
||||
|
||||
@@ -193,10 +222,13 @@ By default, containers have full outbound network access. For stricter security,
|
||||
- Auditing software with telemetry or phone-home behavior
|
||||
- Maximum isolation for highly sensitive reviews
|
||||
|
||||
### Example: Claude + GitHub + Package Registries
|
||||
### Example: Agents + GitHub + Package Registries
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -d api.anthropic.com -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d api.openai.com -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d chatgpt.com -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d auth.openai.com -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d github.com -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d raw.githubusercontent.com -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d registry.npmjs.org -j ACCEPT
|
||||
@@ -214,9 +246,16 @@ sudo iptables -A OUTPUT -j DROP
|
||||
|
||||
## Threat Model
|
||||
|
||||
The primary threat this project addresses is **Claude Code running arbitrary commands on your host machine**. When `bypassPermissions` is enabled, Claude executes shell commands, installs packages, and modifies files without confirmation. On a host machine this means it can modify your shell config, `rm -rf` outside the project directory, or abuse locally stored credentials. The devcontainer confines all of that to a disposable container where the blast radius is limited to `/workspace`.
|
||||
The primary threat this project addresses is **coding agents running arbitrary commands on your host machine**. On a host machine, unrestricted agent modes can modify your shell config, delete files outside the project directory, or abuse locally stored credentials. The devcontainer confines that activity to a disposable container where the blast radius is limited to `/workspace` and any explicit mounts you add.
|
||||
|
||||
The container includes common development tooling so you can do all development work inside it - not just run Claude. The intended workflow is: clone a repository, start the devcontainer, and work entirely within it. If your project needs additional runtimes or tools beyond what's included, either add them to the Dockerfile for repeated use or install them ad-hoc with `devc exec`.
|
||||
The container auto-configures:
|
||||
|
||||
- Claude Code `permissions.defaultMode = bypassPermissions`
|
||||
- Codex `approval_policy = "never"` and `sandbox_mode = "danger-full-access"`
|
||||
|
||||
This would be risky on a host machine, but the container itself is the sandbox.
|
||||
|
||||
The container includes common development tooling so you can do all development work inside it - not just run agents. The intended workflow is: clone a repository, start the devcontainer, and work entirely within it. If your project needs additional runtimes or tools beyond what's included, either add them to the Dockerfile for repeated use or install them ad-hoc with `devc exec`.
|
||||
|
||||
For the specific boundaries of what is and isn't isolated, see [Security Model](#security-model) below. One nuance worth calling out: the devcontainer runtime automatically forwards your host's SSH agent socket (`SSH_AUTH_SOCK`) into the container. This lets code inside the container authenticate as you over SSH (e.g., `git push`), but the actual private key material stays on the host and is never exposed to the container.
|
||||
|
||||
@@ -224,11 +263,9 @@ For the specific boundaries of what is and isn't isolated, see [Security Model](
|
||||
|
||||
This devcontainer provides **filesystem isolation** but not complete sandboxing.
|
||||
|
||||
**Sandboxed:** Filesystem (host files inaccessible), processes (isolated from host), package installations (stay in container)
|
||||
**Sandboxed:** Filesystem (host files inaccessible except explicit mounts), processes (isolated from host), package installations (stay in container)
|
||||
|
||||
**Not sandboxed:** Network (full outbound by default—see [Network Isolation](#network-isolation)), git identity (`~/.gitconfig` mounted read-only), SSH agent (socket forwarded, keys stay on host), Docker socket (not mounted by default)
|
||||
|
||||
The container auto-configures `bypassPermissions` mode—Claude runs commands without confirmation. This would be risky on a host machine, but the container itself is the sandbox.
|
||||
**Not sandboxed:** Network (full outbound by default - see [Network Isolation](#network-isolation)), git identity (`~/.gitconfig` mounted read-only), SSH agent (socket forwarded, keys stay on host), Docker socket (not mounted by default)
|
||||
|
||||
## Container Details
|
||||
|
||||
@@ -236,12 +273,13 @@ The container auto-configures `bypassPermissions` mode—Claude runs commands wi
|
||||
|-----------|---------|
|
||||
| Base | Ubuntu 24.04, Node.js 22, Python 3.13 + uv, zsh |
|
||||
| User | `vscode` (passwordless sudo), working dir `/workspace` |
|
||||
| Agent CLIs | Claude Code, OpenAI Codex CLI |
|
||||
| Tools | `rg`, `fd`, `tmux`, `fzf`, `delta`, `iptables`, `ipset` |
|
||||
| Volumes (survive rebuilds) | Command history (`/commandhistory`), Claude config (`~/.claude`), GitHub CLI auth (`~/.config/gh`) |
|
||||
| Volumes (survive rebuilds) | Command history (`/commandhistory`), Claude config (`~/.claude`), Codex config/auth (`~/.codex`), GitHub CLI auth (`~/.config/gh`) |
|
||||
| Host mounts | `~/.gitconfig` (read-only), `.devcontainer/` (read-only) |
|
||||
| Auto-configured | [anthropics](https://github.com/anthropics/claude-code-plugins) + [trailofbits](https://github.com/trailofbits/claude-code-plugins) skills, git-delta |
|
||||
| Auto-configured | Claude skills, Codex Trail of Bits marketplace, Claude `bypassPermissions`, Codex no-approval `danger-full-access`, git-delta |
|
||||
|
||||
Volumes are stored outside the container, so your shell history, Claude settings, and `gh` login persist even after `devc rebuild`. Host `~/.gitconfig` is mounted read-only for git identity.
|
||||
Volumes are stored outside the container, so your shell history, agent settings, and `gh` login persist even after `devc rebuild`. Host `~/.gitconfig` is mounted read-only for git identity.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -257,6 +295,16 @@ npm install -g @devcontainers/cli
|
||||
2. Try rebuilding: `devc rebuild`
|
||||
3. Check logs: `docker logs $(docker ps -lq)`
|
||||
|
||||
### Codex auth not working
|
||||
|
||||
Check the login status inside the container:
|
||||
|
||||
```bash
|
||||
codex login status
|
||||
```
|
||||
|
||||
For a headless container, confirm `CODEX_ACCESS_TOKEN` or `OPENAI_API_KEY` is set on the host, then run `devc rebuild`.
|
||||
|
||||
### GitHub CLI auth not persisting
|
||||
|
||||
The gh volume may need ownership fix:
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json",
|
||||
"name": "Claude Code Sandbox",
|
||||
"name": "AI Agent Sandbox",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
@@ -44,6 +44,7 @@
|
||||
"mounts": [
|
||||
"source=devc-${localWorkspaceFolderBasename}-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
|
||||
"source=devc-${localWorkspaceFolderBasename}-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
|
||||
"source=devc-${localWorkspaceFolderBasename}-codex-${devcontainerId},target=/home/vscode/.codex,type=volume",
|
||||
"source=devc-${localWorkspaceFolderBasename}-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume",
|
||||
"source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly",
|
||||
"source=${localWorkspaceFolder}/.devcontainer,target=/workspace/.devcontainer,type=bind,readonly",
|
||||
@@ -53,6 +54,7 @@
|
||||
"containerEnv": {
|
||||
"NODE_OPTIONS": "--max-old-space-size=4096",
|
||||
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
|
||||
"CODEX_HOME": "/home/vscode/.codex",
|
||||
"POWERLEVEL9K_DISABLE_GITSTATUS": "true",
|
||||
"GIT_CONFIG_GLOBAL": "/home/vscode/.gitconfig.local",
|
||||
"UV_LINK_MODE": "copy",
|
||||
@@ -67,7 +69,9 @@
|
||||
},
|
||||
"remoteEnv": {
|
||||
"CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN:}",
|
||||
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY:}"
|
||||
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY:}",
|
||||
"CODEX_ACCESS_TOKEN": "${localEnv:CODEX_ACCESS_TOKEN:}",
|
||||
"OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY:}"
|
||||
},
|
||||
"initializeCommand": "test -f \"$HOME/.gitconfig\" || touch \"$HOME/.gitconfig\"",
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
||||
|
||||
+26
-7
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Claude Code Devcontainer CLI Helper
|
||||
# AI coding agent devcontainer CLI helper
|
||||
# Provides the `devc` command for managing devcontainers
|
||||
|
||||
# Resolve symlinks to get actual script location
|
||||
@@ -35,7 +35,7 @@ Commands:
|
||||
update Update devc to the latest version
|
||||
template [dir] Copy devcontainer template to directory (default: current)
|
||||
exec <cmd> Execute a command in the running container
|
||||
upgrade Upgrade Claude Code to latest version
|
||||
upgrade [agent] Upgrade agent CLI(s): claude, codex, or all (default: all)
|
||||
mount <host> <cont> Add a mount to the devcontainer (recreates container)
|
||||
sync [project] [--trusted] Sync sessions from devcontainers to host
|
||||
cp <cont> <host> Copy files/directories from container to host
|
||||
@@ -50,7 +50,8 @@ Examples:
|
||||
devc self-install # Install devc to PATH
|
||||
devc update # Update to latest version
|
||||
devc exec ls -la # Run command in container
|
||||
devc upgrade # Upgrade Claude Code to latest
|
||||
devc upgrade # Upgrade Claude Code and Codex to latest
|
||||
devc upgrade codex # Upgrade only Codex
|
||||
devc mount ~/data /data # Add mount to container
|
||||
devc sync # Sync sessions from all devcontainers
|
||||
devc sync crypto # Sync only matching devcontainer
|
||||
@@ -124,6 +125,7 @@ extract_mounts_to_file() {
|
||||
select(
|
||||
(contains("target=/commandhistory,") | not) and
|
||||
(contains("target=/home/vscode/.claude,") | not) and
|
||||
(contains("target=/home/vscode/.codex,") | not) and
|
||||
(contains("target=/home/vscode/.config/gh,") | not) and
|
||||
(contains("target=/home/vscode/.gitconfig,") | not) and
|
||||
(contains("target=/workspace/.devcontainer,") | not)
|
||||
@@ -289,13 +291,30 @@ cmd_exec() {
|
||||
cmd_upgrade() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder)"
|
||||
local target="${1:-all}"
|
||||
|
||||
case "$target" in
|
||||
claude | codex | all)
|
||||
;;
|
||||
*)
|
||||
log_error "Usage: devc upgrade [claude|codex|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
check_devcontainer_cli
|
||||
log_info "Upgrading Claude Code..."
|
||||
|
||||
devcontainer exec --workspace-folder "$workspace_folder" claude update
|
||||
if [[ "$target" == "claude" || "$target" == "all" ]]; then
|
||||
log_info "Upgrading Claude Code..."
|
||||
devcontainer exec --workspace-folder "$workspace_folder" claude update
|
||||
log_success "Claude Code upgraded"
|
||||
fi
|
||||
|
||||
log_success "Claude Code upgraded"
|
||||
if [[ "$target" == "codex" || "$target" == "all" ]]; then
|
||||
log_info "Upgrading Codex..."
|
||||
devcontainer exec --workspace-folder "$workspace_folder" codex update
|
||||
log_success "Codex upgraded"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_mount() {
|
||||
@@ -825,7 +844,7 @@ main() {
|
||||
cmd_exec "$@"
|
||||
;;
|
||||
upgrade)
|
||||
cmd_upgrade
|
||||
cmd_upgrade "$@"
|
||||
;;
|
||||
mount)
|
||||
cmd_mount "$@"
|
||||
|
||||
+132
-4
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-install configuration for Claude Code devcontainer.
|
||||
"""Post-install configuration for AI coding agent devcontainer.
|
||||
|
||||
Runs on container creation to set up:
|
||||
- Onboarding bypass (when CLAUDE_CODE_OAUTH_TOKEN is set)
|
||||
- Claude settings (bypassPermissions mode)
|
||||
- Codex settings and optional headless auth
|
||||
- Tmux configuration (200k history, mouse support)
|
||||
- Directory ownership fixes for mounted volumes
|
||||
"""
|
||||
@@ -35,7 +36,7 @@ def setup_onboarding_bypass():
|
||||
)
|
||||
return
|
||||
|
||||
# When `CLAUDE_CONFIG_DIR` is set, as is done in `devcontainer.json`, `claude` unexpectedly
|
||||
# When `CLAUDE_CONFIG_DIR` is set, as is done in `devcontainer.json`, `claude` unexpectedly
|
||||
# looks for `.claude.json` in *that* folder, instead of in `~`, contradicting the documentation.
|
||||
# See https://github.com/anthropics/claude-code/issues/3833#issuecomment-3694918874
|
||||
claude_json_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home()))
|
||||
@@ -118,6 +119,129 @@ def setup_claude_settings():
|
||||
)
|
||||
|
||||
|
||||
def has_top_level_toml_key(text: str, key: str) -> bool:
|
||||
"""Return whether a TOML document has a top-level key."""
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if stripped.startswith("["):
|
||||
return False
|
||||
if stripped.startswith(f"{key} ") or stripped.startswith(f"{key}="):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def insert_top_level_toml_defaults(text: str, defaults: dict[str, str]) -> str:
|
||||
"""Insert missing top-level TOML defaults before the first table."""
|
||||
missing = {
|
||||
key: value
|
||||
for key, value in defaults.items()
|
||||
if not has_top_level_toml_key(text, key)
|
||||
}
|
||||
if not missing:
|
||||
return text
|
||||
|
||||
default_lines = [
|
||||
"# Container defaults: the devcontainer is the sandbox boundary.",
|
||||
*[f'{key} = "{value}"' for key, value in missing.items()],
|
||||
]
|
||||
|
||||
lines = text.splitlines()
|
||||
insert_at = len(lines)
|
||||
for index, line in enumerate(lines):
|
||||
if line.strip().startswith("["):
|
||||
insert_at = index
|
||||
break
|
||||
|
||||
if insert_at == 0:
|
||||
lines = default_lines + [""] + lines
|
||||
elif insert_at == len(lines):
|
||||
if lines and lines[-1].strip():
|
||||
lines.append("")
|
||||
lines.extend(default_lines)
|
||||
else:
|
||||
lines = lines[:insert_at] + default_lines + [""] + lines[insert_at:]
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def setup_codex_settings():
|
||||
"""Configure Codex CLI for the externally sandboxed devcontainer."""
|
||||
codex_home = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
|
||||
codex_home.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config_file = codex_home / "config.toml"
|
||||
defaults = {
|
||||
"approval_policy": "never",
|
||||
"sandbox_mode": "danger-full-access",
|
||||
"cli_auth_credentials_store": "file",
|
||||
}
|
||||
|
||||
existing = config_file.read_text(encoding="utf-8") if config_file.exists() else ""
|
||||
updated = insert_top_level_toml_defaults(existing, defaults)
|
||||
|
||||
if updated != existing:
|
||||
config_file.write_text(updated, encoding="utf-8")
|
||||
print(
|
||||
f"[post_install] Codex settings configured: {config_file}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[post_install] Codex settings already configured: {config_file}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def setup_codex_auth():
|
||||
"""Persist Codex auth when a headless token or API key is provided."""
|
||||
access_token = os.environ.get("CODEX_ACCESS_TOKEN", "").strip()
|
||||
api_key = os.environ.get("OPENAI_API_KEY", "").strip()
|
||||
|
||||
if access_token:
|
||||
run_codex_login("--with-access-token", access_token, "CODEX_ACCESS_TOKEN")
|
||||
elif api_key:
|
||||
run_codex_login("--with-api-key", api_key, "OPENAI_API_KEY")
|
||||
else:
|
||||
print(
|
||||
"[post_install] No Codex auth env var set, skipping Codex login",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def run_codex_login(flag: str, secret: str, source_name: str):
|
||||
"""Run codex login without printing credential material."""
|
||||
print(f"[post_install] Running codex login from {source_name}...", file=sys.stderr)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["codex", "login", flag],
|
||||
input=secret,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("[post_install] Warning: codex login timed out", file=sys.stderr)
|
||||
return
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
print(
|
||||
f"[post_install] Warning: could not run codex ({e}) - Codex login skipped",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
if result.returncode != 0:
|
||||
print(
|
||||
f"[post_install] Warning: codex login exited {result.returncode}; "
|
||||
"Codex auth was not persisted",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
print("[post_install] Codex auth configured", file=sys.stderr)
|
||||
|
||||
|
||||
def setup_tmux_config():
|
||||
"""Configure tmux with 200k history, mouse support, and vi keys."""
|
||||
tmux_conf = Path.home() / ".tmux.conf"
|
||||
@@ -172,6 +296,7 @@ def fix_directory_ownership():
|
||||
|
||||
dirs_to_fix = [
|
||||
Path.home() / ".claude",
|
||||
Path.home() / ".codex",
|
||||
Path("/commandhistory"),
|
||||
Path.home() / ".config" / "gh",
|
||||
]
|
||||
@@ -214,8 +339,9 @@ def setup_global_gitignore():
|
||||
|
||||
# Create global gitignore with common patterns
|
||||
patterns = """\
|
||||
# Claude Code
|
||||
# Agent CLIs
|
||||
.claude/
|
||||
.codex/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -296,10 +422,12 @@ def main():
|
||||
"""Run all post-install configuration."""
|
||||
print("[post_install] Starting post-install configuration...", file=sys.stderr)
|
||||
|
||||
fix_directory_ownership()
|
||||
setup_onboarding_bypass()
|
||||
setup_claude_settings()
|
||||
setup_codex_settings()
|
||||
setup_codex_auth()
|
||||
setup_tmux_config()
|
||||
fix_directory_ownership()
|
||||
setup_global_gitignore()
|
||||
|
||||
print("[post_install] Configuration complete!", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user