Skip to content
Open
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
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"GOPATH": "/home/vscode/go",
"GOBIN": "/home/vscode/go/bin",
"GH_TOKEN": "${localEnv:GH_TOKEN}",
"PERSONA": "developer"
},
"initializeCommand": "test -f \"$HOME/.gitconfig\" || touch \"$HOME/.gitconfig\"",
Expand Down
94 changes: 94 additions & 0 deletions .devcontainer/post_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,99 @@ def setup_global_gitignore():
)


def _warn_broad_token(token: str, source: str):
"""Warn if token is not a fine-grained PAT (github_pat_*)."""
if token.startswith("github_pat_"):
return
if token.startswith("ghp_"):
kind = "classic PAT (ghp_*)"
elif token.startswith("gho_"):
kind = "OAuth token (gh auth login)"
else:
kind = "non-fine-grained token"
print(
f"[post_install] Warning: {source} is a {kind}. "
f"Fine-grained PATs (github_pat_*) are recommended — "
f"they scope access to specific repos with limited "
f"permissions. Run 'bash scripts/setup-gh-token.sh' "
f"on the host to set one up.",
file=sys.stderr,
)


def setup_gh_auth():
"""Configure GitHub auth for gh CLI and git HTTPS credential helper.

Uses GH_TOKEN env var if set, otherwise checks gh auth status
from the persistent volume. Runs gh auth setup-git to configure
the git credential helper so both gh API calls and git push/pull
work with one token. Never blocks container creation.
"""
gh_token = os.environ.get("GH_TOKEN", "")

if gh_token:
result = subprocess.run(
["gh", "api", "user", "--jq", ".login"],
capture_output=True,
text=True,
env={**os.environ, "GH_TOKEN": gh_token},
)
if result.returncode == 0 and result.stdout.strip():
login = result.stdout.strip()
print(
f"[post_install] GitHub auth via GH_TOKEN: {login}",
file=sys.stderr,
)
_warn_broad_token(gh_token, "GH_TOKEN")
else:
print(
"[post_install] Warning: GH_TOKEN is set but "
"invalid or expired",
file=sys.stderr,
)
return
else:
result = subprocess.run(
["gh", "auth", "status"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(
"[post_install] Warning: No GitHub auth found. "
"Set GH_TOKEN on the host before building, or "
"run 'gh auth login' inside the container.",
file=sys.stderr,
)
if result.stderr.strip():
print(
f"[post_install] gh auth status: "
f"{result.stderr.strip()}",
file=sys.stderr,
)
return
# Check token type from gh auth
token_result = subprocess.run(
["gh", "auth", "token"],
capture_output=True,
text=True,
)
if token_result.returncode == 0 and token_result.stdout.strip():
_warn_broad_token(
token_result.stdout.strip(), "gh auth token"
)

subprocess.run(
["gh", "auth", "setup-git"],
capture_output=True,
)
print(
"[post_install] Git credential helper configured "
"(gh auth setup-git)",
file=sys.stderr,
)


def main():
"""Run all post-install configuration."""
print(
Expand All @@ -398,6 +491,7 @@ def main():
apply_local_overlay()
setup_tmux_config()
setup_global_gitignore()
setup_gh_auth()

print("[post_install] Configuration complete!", file=sys.stderr)

Expand Down
144 changes: 128 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Then inside the session, run `/trailofbits:config`. It walks you through install

**[Configuration](#configuration)**
- [Sandboxing](#sandboxing)
- [GitHub Authentication](#github-authentication)
- [Hooks](#hooks)
- [Plugins and Skills](#plugins-and-skills)
- [MCP Servers](#mcp-servers)
Expand Down Expand Up @@ -141,6 +142,42 @@ claude-local() {

`claude-local` wraps `claude` with the local server env vars and disables telemetry pings that won't reach Anthropic anyway. Use it anywhere you'd normally run `claude`.

If you're using the [devcontainer](#devcontainer), also add:

```bash
export CLAUDE_CODE_CONFIG_DIR="$HOME/Repos/claude-code-config" # adjust path

claude-container() {
local persona="${1:-developer}"
local project_dir="$PWD"
local project_name
project_name=$(basename "$project_dir")

devcontainer up \
--workspace-folder "$CLAUDE_CODE_CONFIG_DIR" \
--mount "source=${project_dir},target=/workspace/${project_name},type=bind" \
--remote-env "PERSONA=${persona}"

devcontainer exec \
--workspace-folder "$CLAUDE_CODE_CONFIG_DIR" \
sh -c "cd '/workspace/${project_name}' && exec claude"
}
```

`claude-container` builds the devcontainer from this repo, bind-mounts your current directory into `/workspace/<project>`, and launches Claude inside it. Pass a persona name as the first argument to override the default (`developer`):

```bash
cd ~/Repos/my-project
claude-container # developer persona
claude-container l1-auditor # L1 auditor persona
```

The mount is set at container creation time. To switch projects, stop the container and re-run from the new directory, or add `--remove-existing-container` to force a rebuild:

```bash
devcontainer up --workspace-folder "$CLAUDE_CODE_CONFIG_DIR" --remove-existing-container
```

### Settings

Copy `personas/developer/settings.json` to `~/.claude/settings.json` (or merge entries into your existing file). The `$schema` key enables autocomplete and validation in editors that support JSON Schema. The template includes:
Expand Down Expand Up @@ -193,7 +230,7 @@ Review and customize it for your own preferences. The template is opinionated --
|---------|-----|--------|
| **developer** | General software development, code review, feature work | Complete |
| **l1-auditor** | L1 blockchain security: Cosmos SDK, Geth, consensus-execution coupling | Complete |
| **web3-auditor** | Smart contract auditing, blockchain security | Placeholder |
| **web3-auditor** | Smart contract auditing, blockchain security | Complete |
| **pentesting** | Penetration testing, offensive security | Placeholder |

Install a persona by copying its directory contents:
Expand Down Expand Up @@ -257,26 +294,24 @@ Then rebuild the container. The persona's settings.json is installed with `bypas
npm install -g @devcontainers/cli
```

**Option A: CLI (build from this repo, mount any project)**
**Option A: CLI (recommended)**

```bash
# Build and start the container from this repo
devcontainer up --workspace-folder /path/to/claude-code-config
Use the `claude-container` shell function from [Shell Setup](#shell-setup):

# Open a shell inside it
devcontainer exec --workspace-folder /path/to/claude-code-config zsh

# Run Claude Code inside the container
devcontainer exec --workspace-folder /path/to/claude-code-config claude
```bash
cd ~/Repos/my-project
claude-container # developer persona
claude-container l1-auditor # override persona
```

To work on a different project, add a bind mount to `devcontainer.json`:
Or run the commands manually:

```jsonc
"mounts": [
// ... existing mounts ...
"source=/path/to/your-project,target=/workspace/your-project,type=bind"
]
```bash
devcontainer up --workspace-folder /path/to/claude-code-config \
--mount "source=$PWD,target=/workspace/my-project,type=bind"

devcontainer exec --workspace-folder /path/to/claude-code-config \
sh -c "cd /workspace/my-project && claude"
```

**Option B: VS Code / Cursor**
Expand Down Expand Up @@ -334,6 +369,83 @@ For complete isolation from your local machine, run the agent on a disposable cl

- [trailofbits/dropkit](https://github.com/trailofbits/dropkit) -- CLI tool for managing DigitalOcean droplets with automated setup, SSH config, and Tailscale VPN. Create a droplet, SSH in, run Claude Code, destroy it when done.

### GitHub Authentication

Claude Code needs GitHub access for two things: **git operations** (`push`, `pull`, `clone`) over HTTPS, and **GitHub API operations** via `gh` CLI (`pr create`, `issue comment`, `pr review`). A single fine-grained PAT covers both.

#### Creating a fine-grained PAT

1. Go to [GitHub → Settings → Fine-grained tokens](https://github.com/settings/personal-access-tokens/new)
2. Set a descriptive name and expiration
3. Under **Repository access**, select the repos Claude Code will work with
4. Grant these permissions:

| Permission | Access | Used for |
|------------|--------|----------|
| Contents | Read and write | `git push`, `git pull`, branch creation |
| Issues | Read and write | Read and comment on issues |
| Pull requests | Read and write | Create, review, and comment on PRs |
| Metadata | Read | Auto-granted with any repo access |

#### Automated setup (macOS)

```bash
bash scripts/setup-gh-token.sh
```

On macOS, the script stores the token in the Keychain and writes a `security find-generic-password` lookup to the shell profile — no plaintext token in dotfiles. On other platforms, it falls back to a plaintext export. Re-running detects existing auth and skips. Use `--remove` to clean up:

```bash
bash scripts/setup-gh-token.sh --remove
```

#### Manual setup

```bash
# macOS: store in Keychain, export from Keychain
security add-generic-password -s "claude-code" -a "github-pat" -w "ghp_your_token_here" -U
echo 'export GH_TOKEN=$(security find-generic-password -s "claude-code" -a "github-pat" -w 2>/dev/null)' >> ~/.zshrc

# Linux: plaintext export
echo "export GH_TOKEN='ghp_your_token_here'" >> ~/.bashrc

# Configure git credential helper for HTTPS push/pull
gh auth setup-git
```

#### Devcontainer

The `GH_TOKEN` env var is automatically passed from your host into the container via `containerEnv` in `devcontainer.json`. On container creation, `post_install.py` runs `gh auth setup-git` to configure the git credential helper. If `GH_TOKEN` is not set on the host, the container falls back to the persistent `~/.config/gh` volume -- run `gh auth login` inside the container as an alternative.

#### Notes

- Repos must use **HTTPS URLs** (not SSH). The credential helper only works with HTTPS. Convert with: `git remote set-url origin https://github.com/owner/repo.git`
- **Security model:** Fine-grained PATs scope access to specific repos with limited permissions and forced expiration. The `settings.json` deny rules block `Read(~/.config/gh/**)` so Claude can't exfiltrate stored credentials. Inside the devcontainer, the token is only available as an env var within the container's isolated filesystem.

#### SSH commit signing

If your git config requires SSH-signed commits (`commit.gpgsign = true`, `gpg.format = ssh`), the Bash sandbox will block signing because it denies access to both `~/.ssh/` and the `SSH_AUTH_SOCK` Unix socket. Two setup steps are needed:

**1. Move the public key outside `~/.ssh/`:**

Git needs to read the `.pub` file referenced in `user.signingkey`. The sandbox denies all reads from `~/.ssh/`, so copy the public key to an allowed location:

```bash
mkdir -p ~/.config/git
cp ~/.ssh/your_signing_key.pub ~/.config/git/signing-key.pub
git config --global user.signingkey ~/.config/git/signing-key.pub
```

**2. Load the private key into ssh-agent before starting Claude:**

```bash
ssh-add ~/.ssh/your_signing_key
```

The agent holds the private key in memory. Claude never accesses the private key file -- git talks to the agent via `SSH_AUTH_SOCK` to perform signing.

**How Claude commits with signing:** The CLAUDE.md instructs Claude to detect SSH signing and use `dangerouslyDisableSandbox: true` on `git commit` Bash calls so git can reach the ssh-agent socket. This only lifts the Bash tool sandbox for that single command -- the OS-level Seatbelt sandbox still restricts writes to the project directory, and Claude's `Read`/`Edit` deny rules for `~/.ssh/**` remain active. If the agent isn't loaded, Claude will ask you to run `ssh-add`.

### Hooks

Hooks are shell commands (or LLM prompts) that fire at specific points in Claude Code's lifecycle. They are a way to talk to the LLM at decision points it wouldn't otherwise pause at. Every `PreToolUse` hook is a chance to say "stop, think about this" or "don't do that, do this instead." Every `PostToolUse` hook is a chance to say "now that you did that, here's what you should know." Every `Stop` hook is a chance to say "you're not done yet."
Expand Down
10 changes: 10 additions & 0 deletions personas/developer/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ Pin actions to SHA hashes with version comments: `actions/checkout@<full-sha> #
- Never push directly to main — use feature branches and PRs
- Never commit secrets, API keys, or credentials — use `.env` files (gitignored) and environment variables

**SSH commit signing:**
This repo requires SSH-signed commits (`commit.gpgsign = true`, `gpg.format = ssh`). The Bash sandbox blocks access to `~/.ssh/` and the `SSH_AUTH_SOCK` Unix socket, so git cannot sign commits in sandboxed mode. Before your first commit in a session:
1. Check if signing is configured: `git config --get commit.gpgsign`
2. If yes, verify the signing key is accessible: run `ssh-add -l` (may need `dangerouslyDisableSandbox`)
3. If the agent returns "Operation not permitted" or "No such file or directory", ask the user to run `ssh-add` to load their key, then retry
4. All `git commit` calls that require signing must use `dangerouslyDisableSandbox: true` on the Bash tool so git can reach the ssh-agent
5. The `user.signingkey` must point to a `.pub` file outside `~/.ssh/` (e.g. `~/.config/git/signing-key.pub`) since the sandbox denies reads from `~/.ssh/`

This is safe: `dangerouslyDisableSandbox` only lifts the Bash tool sandbox for that one command. The OS-level Seatbelt sandbox (`sandbox.enabled: true`) still restricts filesystem writes to the project directory, and Claude's `Read`/`Edit` deny rules for `~/.ssh/**` remain active.

**Hooks and worktrees:**
- Install prek in every repo (`prek install`). Run `prek run` before committing. Configure auto-updates: `prek auto-update --cooldown-days 7`
- Parallel subagents require worktrees. Each subagent MUST work in its own worktree (`wt switch <branch>`), not the main repo. Never share working directories.
Expand Down
Loading