mirror of
https://github.com/trailofbits/claude-code-devcontainer
synced 2026-06-21 14:11:48 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Read(.devcontainer/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
*.md
|
||||
.github
|
||||
.vscode
|
||||
*.log
|
||||
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
.ruff_cache/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
@@ -0,0 +1,56 @@
|
||||
# Zsh configuration for Claude Code devcontainer
|
||||
|
||||
# Add Claude Code to PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# fnm (Fast Node Manager)
|
||||
export FNM_DIR="$HOME/.fnm"
|
||||
export PATH="$FNM_DIR:$PATH"
|
||||
eval "$(fnm env --use-on-cd)"
|
||||
|
||||
# History settings
|
||||
export HISTFILE=/commandhistory/.zsh_history
|
||||
export HISTSIZE=200000
|
||||
export SAVEHIST=200000
|
||||
setopt SHARE_HISTORY
|
||||
setopt HIST_IGNORE_DUPS
|
||||
setopt HIST_IGNORE_ALL_DUPS # Remove older duplicate entries
|
||||
setopt HIST_REDUCE_BLANKS # Remove extra blanks from commands
|
||||
setopt HIST_VERIFY # Show command before executing from history
|
||||
|
||||
# Directory navigation
|
||||
setopt AUTO_CD # cd by typing directory name
|
||||
setopt AUTO_PUSHD # Push directories onto stack
|
||||
setopt PUSHD_IGNORE_DUPS # Don't push duplicates
|
||||
setopt PUSHD_SILENT # Don't print stack after pushd/popd
|
||||
|
||||
# Completion
|
||||
setopt COMPLETE_IN_WORD # Complete from both ends of word
|
||||
setopt ALWAYS_TO_END # Move cursor to end after completion
|
||||
|
||||
# Aliases
|
||||
alias fd=fdfind
|
||||
alias sg=ast-grep
|
||||
alias claude-yolo='claude --dangerously-skip-permissions'
|
||||
alias ll='ls -lah --color=auto'
|
||||
alias la='ls -A --color=auto'
|
||||
alias l='ls -CF --color=auto'
|
||||
alias grep='grep --color=auto'
|
||||
|
||||
# fzf configuration - use fd for faster file finding
|
||||
export FZF_DEFAULT_COMMAND='fdfind --type f --hidden --follow --exclude .git'
|
||||
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
|
||||
export FZF_ALT_C_COMMAND='fdfind --type d --hidden --follow --exclude .git'
|
||||
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border --info=inline'
|
||||
|
||||
# Use fd for ** completion (e.g., vim **)
|
||||
_fzf_compgen_path() {
|
||||
fdfind --hidden --follow --exclude .git . "$1"
|
||||
}
|
||||
_fzf_compgen_dir() {
|
||||
fdfind --type d --hidden --follow --exclude .git . "$1"
|
||||
}
|
||||
|
||||
# Source fzf key bindings and completion
|
||||
source ~/.fzf/key-bindings.zsh
|
||||
source ~/.fzf/completion.zsh
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# Claude Code Devcontainer
|
||||
# Based on Microsoft devcontainer image for better devcontainer integration
|
||||
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
|
||||
|
||||
ARG TZ
|
||||
ENV TZ="$TZ"
|
||||
|
||||
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 \
|
||||
# Modern CLI tools
|
||||
fzf \
|
||||
ripgrep \
|
||||
fd-find \
|
||||
tmux \
|
||||
zsh \
|
||||
# Build tools
|
||||
build-essential \
|
||||
# Utilities
|
||||
jq \
|
||||
nano \
|
||||
vim \
|
||||
unzip \
|
||||
# Network tools (for security testing)
|
||||
iptables \
|
||||
ipset \
|
||||
iproute2 \
|
||||
dnsutils \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install git-delta
|
||||
ARG GIT_DELTA_VERSION=0.18.2
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
curl -fsSL "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" -o /tmp/git-delta.deb && \
|
||||
dpkg -i /tmp/git-delta.deb && \
|
||||
rm /tmp/git-delta.deb
|
||||
|
||||
# Install uv (Python package manager) via multi-stage copy
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
# Create directories and set ownership (combined for fewer layers)
|
||||
RUN mkdir -p /commandhistory /workspace /home/vscode/.claude /opt && \
|
||||
touch /commandhistory/.bash_history && \
|
||||
touch /commandhistory/.zsh_history && \
|
||||
chown -R vscode:vscode /commandhistory /workspace /home/vscode/.claude /opt
|
||||
|
||||
# Set environment variables
|
||||
ENV DEVCONTAINER=true
|
||||
ENV SHELL=/bin/zsh
|
||||
ENV EDITOR=nano
|
||||
ENV VISUAL=nano
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# Switch to non-root user for remaining setup
|
||||
USER vscode
|
||||
|
||||
# Set PATH early so claude and other user-installed binaries are available
|
||||
ENV PATH="/home/vscode/.local/bin:$PATH"
|
||||
|
||||
# Install Claude Code natively with marketplace plugins
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
||||
claude plugin marketplace add anthropics/skills && \
|
||||
claude plugin marketplace add trailofbits/skills
|
||||
|
||||
# Install Python 3.13 via uv (fast binary download, not source compilation)
|
||||
RUN uv python install 3.13 --default
|
||||
|
||||
# Install ast-grep (AST-based code search)
|
||||
RUN uv tool install ast-grep-cli
|
||||
|
||||
# Install fnm (Fast Node Manager) and Node 22
|
||||
ARG NODE_VERSION=22
|
||||
ENV FNM_DIR="/home/vscode/.fnm"
|
||||
RUN curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir "$FNM_DIR" --skip-shell && \
|
||||
export PATH="$FNM_DIR:$PATH" && \
|
||||
eval "$(fnm env)" && \
|
||||
fnm install ${NODE_VERSION} && \
|
||||
fnm default ${NODE_VERSION}
|
||||
|
||||
# Install Oh My Zsh
|
||||
ARG ZSH_IN_DOCKER_VERSION=1.2.1
|
||||
RUN sh -c "$(curl -fsSL https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
|
||||
-p git \
|
||||
-x
|
||||
|
||||
# Download fzf shell integration (Ubuntu 24.04 package doesn't include these)
|
||||
ARG FZF_VERSION=0.44.1
|
||||
RUN mkdir -p /home/vscode/.fzf && \
|
||||
curl -fsSL "https://raw.githubusercontent.com/junegunn/fzf/${FZF_VERSION}/shell/key-bindings.zsh" -o /home/vscode/.fzf/key-bindings.zsh && \
|
||||
curl -fsSL "https://raw.githubusercontent.com/junegunn/fzf/${FZF_VERSION}/shell/completion.zsh" -o /home/vscode/.fzf/completion.zsh
|
||||
|
||||
# Copy zsh configuration
|
||||
COPY --chown=vscode:vscode .zshrc /home/vscode/.zshrc.custom
|
||||
|
||||
# Append custom zshrc to the main one
|
||||
RUN echo 'source ~/.zshrc.custom' >> /home/vscode/.zshrc
|
||||
|
||||
# Copy post_install script
|
||||
COPY --chown=vscode:vscode post_install.py /opt/post_install.py
|
||||
@@ -0,0 +1,190 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2025 Trail of Bits, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Claude Code 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.
|
||||
|
||||
## 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.
|
||||
|
||||
**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
|
||||
- **Multi-repo engagements**: Work on multiple related repositories
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker runtime** (one of):
|
||||
- [Docker Desktop](https://docker.com/products/docker-desktop) - ensure it's running
|
||||
- [OrbStack](https://orbstack.dev/)
|
||||
- [Colima](https://github.com/abiosoft/colima): `brew install colima docker && colima start`
|
||||
|
||||
- **For terminal workflows** (one-time install):
|
||||
|
||||
```bash
|
||||
npm install -g @devcontainers/cli
|
||||
git clone https://github.com/trailofbits/claude-code-devcontainer ~/.claude-devcontainer
|
||||
~/.claude-devcontainer/install.sh self-install
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>Optimizing Colima for Apple Silicon</strong></summary>
|
||||
|
||||
Colima's defaults (QEMU + sshfs) are conservative. For better performance:
|
||||
|
||||
```bash
|
||||
# Stop and delete current VM (removes containers/images)
|
||||
colima stop && colima delete
|
||||
|
||||
# Start with optimized settings
|
||||
colima start \
|
||||
--cpu 4 \
|
||||
--memory 8 \
|
||||
--disk 100 \
|
||||
--vm-type vz \
|
||||
--vz-rosetta \
|
||||
--mount-type virtiofs
|
||||
```
|
||||
|
||||
Adjust `--cpu` and `--memory` based on your Mac (e.g., 6/16 for Pro, 8/32 for Max).
|
||||
|
||||
| Option | Benefit |
|
||||
|--------|---------|
|
||||
| `--vm-type vz` | Apple Virtualization.framework (faster than QEMU) |
|
||||
| `--mount-type virtiofs` | 5-10x faster file I/O than sshfs |
|
||||
| `--vz-rosetta` | Run x86 containers via Rosetta |
|
||||
|
||||
Verify with `colima status` - should show "macOS Virtualization.Framework" and "virtiofs".
|
||||
|
||||
</details>
|
||||
|
||||
## Quick Start
|
||||
|
||||
Choose the pattern that fits your workflow:
|
||||
|
||||
### Pattern A: Per-Project Container (Isolated)
|
||||
|
||||
Each project gets its own container with independent volumes. Best for one-off reviews, untrusted repos, or when you need isolation between projects.
|
||||
|
||||
**Terminal:**
|
||||
|
||||
```bash
|
||||
git clone <untrusted-repo>
|
||||
cd untrusted-repo
|
||||
devc . # Installs template + starts container
|
||||
devc shell # Opens shell in container
|
||||
```
|
||||
|
||||
**VS Code / Cursor:**
|
||||
|
||||
1. Install the Dev Containers extension:
|
||||
- VS Code: `ms-vscode-remote.remote-containers`
|
||||
- Cursor: `anysphere.remote-containers`
|
||||
|
||||
2. Set up the devcontainer (choose one):
|
||||
|
||||
```bash
|
||||
# Option A: Use devc (recommended)
|
||||
devc .
|
||||
|
||||
# Option B: Clone manually
|
||||
git clone https://github.com/trailofbits/claude-code-devcontainer .devcontainer/
|
||||
```
|
||||
|
||||
3. Open **your project folder** in VS Code, then:
|
||||
- Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux)
|
||||
- Type "Reopen in Container" and select **Dev Containers: Reopen in Container**
|
||||
|
||||
### Pattern B: Shared Workspace Container (Grouped)
|
||||
|
||||
A parent directory contains the devcontainer config, and you clone multiple repos inside. Shared volumes across all repos. Best for client engagements, related repositories, or ongoing work.
|
||||
|
||||
```bash
|
||||
# Create workspace for a client engagement
|
||||
mkdir -p ~/sandbox/client-name
|
||||
cd ~/sandbox/client-name
|
||||
devc . # Install template + start container
|
||||
devc shell # Opens shell in container
|
||||
|
||||
# Inside container:
|
||||
git clone <client-repo-1>
|
||||
git clone <client-repo-2>
|
||||
cd client-repo-1
|
||||
claude # Ready to work
|
||||
```
|
||||
|
||||
## CLI Helper Commands
|
||||
|
||||
```
|
||||
devc . Install template + start container in current directory
|
||||
devc up Start the devcontainer
|
||||
devc rebuild Rebuild container (preserves persistent volumes)
|
||||
devc down Stop the container
|
||||
devc shell Open zsh shell in container
|
||||
devc template DIR Copy devcontainer files to directory
|
||||
devc self-install Install devc to ~/.local/bin
|
||||
```
|
||||
|
||||
## Network Isolation
|
||||
|
||||
By default, containers have full outbound network access. For stricter security, use iptables to restrict network access.
|
||||
|
||||
### When to Enable Network Isolation
|
||||
|
||||
- Reviewing code that may contain malicious dependencies
|
||||
- Auditing software with telemetry or phone-home behavior
|
||||
- Maximum isolation for highly sensitive reviews
|
||||
|
||||
### Example: Claude + GitHub + Package Registries
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -d api.anthropic.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
|
||||
sudo iptables -A OUTPUT -d pypi.org -j ACCEPT
|
||||
sudo iptables -A OUTPUT -d files.pythonhosted.org -j ACCEPT
|
||||
sudo iptables -A OUTPUT -o lo -j ACCEPT
|
||||
sudo iptables -A OUTPUT -j DROP
|
||||
```
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- Blocks package managers unless you allowlist registries
|
||||
- May break tools that require network access
|
||||
- DNS resolution still works (consider blocking if paranoid)
|
||||
|
||||
## 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)
|
||||
|
||||
**Not sandboxed:** Network (full outbound by default—see [Network Isolation](#network-isolation)), git identity (`~/.gitconfig` mounted read-only), 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.
|
||||
|
||||
## Container Details
|
||||
|
||||
| Component | Details |
|
||||
|-----------|---------|
|
||||
| Base | Ubuntu 24.04, Node.js 22, Python 3.13 + uv, zsh |
|
||||
| User | `vscode` (passwordless sudo), working dir `/workspace` |
|
||||
| Tools | `rg`, `fd`, `tmux`, `fzf`, `delta`, `iptables`, `ipset` |
|
||||
| Volumes (survive rebuilds) | Command history, Claude config, GitHub CLI auth |
|
||||
| Auto-configured | [anthropics](https://github.com/anthropics/claude-code-plugins) + [trailofbits](https://github.com/trailofbits/claude-code-plugins) skills, 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.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "devcontainer CLI not found"
|
||||
|
||||
```bash
|
||||
npm install -g @devcontainers/cli
|
||||
```
|
||||
|
||||
### Container won't start
|
||||
|
||||
1. Check Docker is running
|
||||
2. Try rebuilding: `devc rebuild`
|
||||
3. Check logs: `docker logs $(docker ps -lq)`
|
||||
|
||||
### GitHub CLI auth not persisting
|
||||
|
||||
The gh volume may need ownership fix:
|
||||
|
||||
```bash
|
||||
sudo chown -R $(id -u):$(id -g) ~/.config/gh
|
||||
```
|
||||
|
||||
### Python/uv not working
|
||||
|
||||
Python is managed via uv:
|
||||
|
||||
```bash
|
||||
uv run script.py # Run a script
|
||||
uv add package # Add project dependency
|
||||
uv run --with requests py.py # Ad-hoc dependency
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Build the image manually:
|
||||
|
||||
```bash
|
||||
devcontainer build --workspace-folder .
|
||||
```
|
||||
|
||||
Test the container:
|
||||
|
||||
```bash
|
||||
devcontainer up --workspace-folder .
|
||||
devcontainer exec --workspace-folder . zsh
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json",
|
||||
"name": "Claude Code Sandbox",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
"TZ": "${localEnv:TZ:America/New_York}",
|
||||
"GIT_DELTA_VERSION": "0.18.2",
|
||||
"ZSH_IN_DOCKER_VERSION": "1.2.1"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/tailscale/codespace/tailscale:1": {}
|
||||
},
|
||||
"runArgs": [
|
||||
"--cap-add=NET_ADMIN",
|
||||
"--cap-add=NET_RAW"
|
||||
],
|
||||
"init": true,
|
||||
"updateRemoteUserUID": true,
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"anthropic.claude-code"
|
||||
],
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "zsh",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"icon": "terminal-bash"
|
||||
},
|
||||
"zsh": {
|
||||
"path": "zsh"
|
||||
}
|
||||
},
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"remoteUser": "vscode",
|
||||
"mounts": [
|
||||
"source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
|
||||
"source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
|
||||
"source=claude-code-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume",
|
||||
"source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly"
|
||||
],
|
||||
"containerEnv": {
|
||||
"NODE_OPTIONS": "--max-old-space-size=4096",
|
||||
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
|
||||
"POWERLEVEL9K_DISABLE_GITSTATUS": "true",
|
||||
"GIT_CONFIG_GLOBAL": "/home/vscode/.gitconfig.local",
|
||||
"UV_LINK_MODE": "copy",
|
||||
"NPM_CONFIG_IGNORE_SCRIPTS": "true",
|
||||
"NPM_CONFIG_AUDIT": "true",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_SAVE_EXACT": "true",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"NPM_CONFIG_MINIMUM_RELEASE_AGE": "1440",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK": "1"
|
||||
},
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
||||
"workspaceFolder": "/workspace",
|
||||
"postCreateCommand": "uv run --no-project /opt/post_install.py"
|
||||
}
|
||||
Executable
+255
@@ -0,0 +1,255 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Claude Code Devcontainer CLI Helper
|
||||
# Provides the `devc` command for managing devcontainers
|
||||
|
||||
# Resolve symlinks to get actual script location
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [[ -L "$SOURCE" ]]; do
|
||||
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
||||
done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_usage() {
|
||||
cat <<EOF
|
||||
Usage: devc <command> [options]
|
||||
|
||||
Commands:
|
||||
. Install devcontainer template to current directory and start
|
||||
up Start the devcontainer in current directory
|
||||
rebuild Rebuild the devcontainer (preserves auth volumes)
|
||||
down Stop the devcontainer
|
||||
shell Open a shell in the running container
|
||||
self-install Install 'devc' command to ~/.local/bin
|
||||
update Update devc to the latest version
|
||||
template [dir] Copy devcontainer template to directory (default: current)
|
||||
help Show this help message
|
||||
|
||||
Examples:
|
||||
devc . # Install template and start container
|
||||
devc up # Start container in current directory
|
||||
devc rebuild # Clean rebuild
|
||||
devc shell # Open interactive shell
|
||||
devc self-install # Install devc to PATH
|
||||
devc update # Update to latest version
|
||||
EOF
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[devc]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[devc]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[devc]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[devc]${NC} $1" >&2
|
||||
}
|
||||
|
||||
check_devcontainer_cli() {
|
||||
if ! command -v devcontainer &>/dev/null; then
|
||||
log_error "devcontainer CLI not found."
|
||||
log_info "Install it with: npm install -g @devcontainers/cli"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_workspace_folder() {
|
||||
echo "${1:-$(pwd)}"
|
||||
}
|
||||
|
||||
cmd_template() {
|
||||
local target_dir="${1:-.}"
|
||||
target_dir="$(cd "$target_dir" 2>/dev/null && pwd)" || {
|
||||
log_error "Directory does not exist: $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
local devcontainer_dir="$target_dir/.devcontainer"
|
||||
|
||||
if [[ -d "$devcontainer_dir" ]]; then
|
||||
log_warn "Devcontainer already exists at $devcontainer_dir"
|
||||
read -p "Overwrite? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$devcontainer_dir"
|
||||
|
||||
# Copy template files
|
||||
cp "$SCRIPT_DIR/Dockerfile" "$devcontainer_dir/"
|
||||
cp "$SCRIPT_DIR/devcontainer.json" "$devcontainer_dir/"
|
||||
cp "$SCRIPT_DIR/post_install.py" "$devcontainer_dir/"
|
||||
cp "$SCRIPT_DIR/.zshrc" "$devcontainer_dir/"
|
||||
|
||||
log_success "Template installed to $devcontainer_dir"
|
||||
}
|
||||
|
||||
cmd_up() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder "${1:-}")"
|
||||
|
||||
check_devcontainer_cli
|
||||
log_info "Starting devcontainer in $workspace_folder..."
|
||||
|
||||
devcontainer up --workspace-folder "$workspace_folder"
|
||||
log_success "Devcontainer started"
|
||||
}
|
||||
|
||||
cmd_rebuild() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder "${1:-}")"
|
||||
|
||||
check_devcontainer_cli
|
||||
log_info "Rebuilding devcontainer in $workspace_folder..."
|
||||
|
||||
devcontainer up --workspace-folder "$workspace_folder" --remove-existing-container
|
||||
log_success "Devcontainer rebuilt"
|
||||
}
|
||||
|
||||
cmd_down() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder "${1:-}")"
|
||||
|
||||
check_devcontainer_cli
|
||||
log_info "Stopping devcontainer..."
|
||||
|
||||
# Get container ID and stop it
|
||||
local container_id
|
||||
container_id=$(docker ps -q --filter "label=devcontainer.local_folder=$workspace_folder" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$container_id" ]]; then
|
||||
docker stop "$container_id"
|
||||
log_success "Devcontainer stopped"
|
||||
else
|
||||
log_warn "No running devcontainer found for $workspace_folder"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder)"
|
||||
|
||||
check_devcontainer_cli
|
||||
log_info "Opening shell in devcontainer..."
|
||||
|
||||
devcontainer exec --workspace-folder "$workspace_folder" zsh
|
||||
}
|
||||
|
||||
cmd_self_install() {
|
||||
local install_dir="$HOME/.local/bin"
|
||||
local install_path="$install_dir/devc"
|
||||
|
||||
mkdir -p "$install_dir"
|
||||
|
||||
# Create a symlink to the original script
|
||||
ln -sf "$SCRIPT_DIR/$SCRIPT_NAME" "$install_path"
|
||||
|
||||
log_success "Installed 'devc' to $install_path"
|
||||
|
||||
# Check if in PATH
|
||||
if [[ ":$PATH:" != *":$install_dir:"* ]]; then
|
||||
log_warn "$install_dir is not in your PATH"
|
||||
log_info "Add this to your shell profile:"
|
||||
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
log_info "Updating devc..."
|
||||
|
||||
if ! git -C "$SCRIPT_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
log_error "Not a git repository: $SCRIPT_DIR"
|
||||
log_info "Re-clone with: rm -rf ~/.claude-devcontainer && git clone https://github.com/trailofbits/claude-code-devcontainer ~/.claude-devcontainer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local before_sha after_sha
|
||||
before_sha=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
|
||||
|
||||
if ! git -C "$SCRIPT_DIR" pull --ff-only; then
|
||||
log_error "Update failed. Try: cd $SCRIPT_DIR && git pull"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
after_sha=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
|
||||
|
||||
if [[ "$before_sha" == "$after_sha" ]]; then
|
||||
log_success "Already up to date"
|
||||
else
|
||||
log_success "Updated from ${before_sha:0:7} to ${after_sha:0:7}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dot() {
|
||||
# Install template and start container in one command
|
||||
cmd_template "."
|
||||
cmd_up "."
|
||||
}
|
||||
|
||||
# Main command dispatcher
|
||||
main() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
print_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local command="$1"
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
.)
|
||||
cmd_dot
|
||||
;;
|
||||
up)
|
||||
cmd_up "$@"
|
||||
;;
|
||||
rebuild)
|
||||
cmd_rebuild "$@"
|
||||
;;
|
||||
down)
|
||||
cmd_down "$@"
|
||||
;;
|
||||
shell)
|
||||
cmd_shell
|
||||
;;
|
||||
self-install)
|
||||
cmd_self_install
|
||||
;;
|
||||
update)
|
||||
cmd_update
|
||||
;;
|
||||
template)
|
||||
cmd_template "$@"
|
||||
;;
|
||||
help | --help | -h)
|
||||
print_usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $command"
|
||||
print_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-install configuration for Claude Code devcontainer.
|
||||
|
||||
Runs on container creation to set up:
|
||||
- Claude settings (bypassPermissions mode)
|
||||
- Tmux configuration (200k history, mouse support)
|
||||
- Directory ownership fixes for mounted volumes
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_claude_settings():
|
||||
"""Configure Claude Code with bypassPermissions enabled."""
|
||||
claude_dir = Path.home() / ".claude"
|
||||
claude_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
settings_file = claude_dir / "settings.json"
|
||||
|
||||
# Load existing settings or start fresh
|
||||
settings = {}
|
||||
if settings_file.exists():
|
||||
try:
|
||||
settings = json.loads(settings_file.read_text())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Set bypassPermissions mode
|
||||
if "permissions" not in settings:
|
||||
settings["permissions"] = {}
|
||||
settings["permissions"]["defaultMode"] = "bypassPermissions"
|
||||
|
||||
settings_file.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[post_install] Claude settings configured: {settings_file}", file=sys.stderr)
|
||||
|
||||
|
||||
def setup_tmux_config():
|
||||
"""Configure tmux with 200k history, mouse support, and vi keys."""
|
||||
tmux_conf = Path.home() / ".tmux.conf"
|
||||
|
||||
if tmux_conf.exists():
|
||||
print("[post_install] Tmux config exists, skipping", file=sys.stderr)
|
||||
return
|
||||
|
||||
config = """\
|
||||
# 200k line scrollback history
|
||||
set-option -g history-limit 200000
|
||||
|
||||
# Enable mouse support
|
||||
set -g mouse on
|
||||
|
||||
# Use vi keys in copy mode
|
||||
setw -g mode-keys vi
|
||||
|
||||
# Start windows and panes at 1, not 0
|
||||
set -g base-index 1
|
||||
setw -g pane-base-index 1
|
||||
|
||||
# Renumber windows when one is closed
|
||||
set -g renumber-windows on
|
||||
|
||||
# Faster escape time for vim
|
||||
set -sg escape-time 10
|
||||
|
||||
# True color support
|
||||
set -g default-terminal "tmux-256color"
|
||||
set -ag terminal-overrides ",xterm-256color:RGB"
|
||||
|
||||
# Terminal features (ghostty, cursor shape in vim)
|
||||
set -as terminal-features ",xterm-ghostty:RGB"
|
||||
set -as terminal-features ",xterm*:RGB"
|
||||
set -ga terminal-overrides ",xterm*:colors=256"
|
||||
set -ga terminal-overrides '*:Ss=\\E[%p1%d q:Se=\\E[ q'
|
||||
|
||||
# Status bar
|
||||
set -g status-style 'bg=#333333 fg=#ffffff'
|
||||
set -g status-left '[#S] '
|
||||
set -g status-right '%Y-%m-%d %H:%M'
|
||||
"""
|
||||
tmux_conf.write_text(config, encoding="utf-8")
|
||||
print(f"[post_install] Tmux configured: {tmux_conf}", file=sys.stderr)
|
||||
|
||||
|
||||
def fix_directory_ownership():
|
||||
"""Fix ownership of mounted volumes that may have root ownership."""
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
|
||||
dirs_to_fix = [
|
||||
Path.home() / ".claude",
|
||||
Path("/commandhistory"),
|
||||
Path.home() / ".config" / "gh",
|
||||
]
|
||||
|
||||
for dir_path in dirs_to_fix:
|
||||
if dir_path.exists():
|
||||
try:
|
||||
# Use sudo to fix ownership if needed
|
||||
stat_info = dir_path.stat()
|
||||
if stat_info.st_uid != uid:
|
||||
subprocess.run(
|
||||
["sudo", "chown", "-R", f"{uid}:{gid}", str(dir_path)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
print(f"[post_install] Fixed ownership: {dir_path}", file=sys.stderr)
|
||||
except (PermissionError, subprocess.CalledProcessError) as e:
|
||||
print(
|
||||
f"[post_install] Warning: Could not fix ownership of {dir_path}: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def setup_global_gitignore():
|
||||
"""Set up global gitignore and local git config.
|
||||
|
||||
Since ~/.gitconfig is mounted read-only from host, we create a local
|
||||
config file that includes the host config and adds container-specific
|
||||
settings like core.excludesfile and delta configuration.
|
||||
|
||||
GIT_CONFIG_GLOBAL env var (set in devcontainer.json) points git to this
|
||||
local config as the "global" config.
|
||||
"""
|
||||
home = Path.home()
|
||||
gitignore = home / ".gitignore_global"
|
||||
local_gitconfig = home / ".gitconfig.local"
|
||||
host_gitconfig = home / ".gitconfig"
|
||||
|
||||
# Create global gitignore with common patterns
|
||||
patterns = """\
|
||||
# Claude Code
|
||||
.claude/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
|
||||
# Python
|
||||
*.pyc
|
||||
*.pyo
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
*.egg
|
||||
.venv/
|
||||
venv/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
.npm/
|
||||
|
||||
# Editors
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.idea/
|
||||
.vscode/
|
||||
*.sublime-*
|
||||
|
||||
# Misc
|
||||
*.log
|
||||
.env.local
|
||||
.env.*.local
|
||||
"""
|
||||
gitignore.write_text(patterns, encoding="utf-8")
|
||||
print(f"[post_install] Global gitignore created: {gitignore}", file=sys.stderr)
|
||||
|
||||
# Create local git config that includes host config and sets excludesfile + delta
|
||||
# Delta config is included here so it works even if host doesn't have it configured
|
||||
local_config = f"""\
|
||||
# Container-local git config
|
||||
# Includes host config (mounted read-only) and adds container settings
|
||||
|
||||
[include]
|
||||
path = {host_gitconfig}
|
||||
|
||||
[core]
|
||||
excludesfile = {gitignore}
|
||||
pager = delta
|
||||
|
||||
[interactive]
|
||||
diffFilter = delta --color-only
|
||||
|
||||
[delta]
|
||||
navigate = true
|
||||
light = false
|
||||
line-numbers = true
|
||||
side-by-side = false
|
||||
|
||||
[merge]
|
||||
conflictstyle = diff3
|
||||
|
||||
[diff]
|
||||
colorMoved = default
|
||||
"""
|
||||
local_gitconfig.write_text(local_config, encoding="utf-8")
|
||||
print(f"[post_install] Local git config created: {local_gitconfig}", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all post-install configuration."""
|
||||
print("[post_install] Starting post-install configuration...", file=sys.stderr)
|
||||
|
||||
setup_claude_settings()
|
||||
setup_tmux_config()
|
||||
fix_directory_ownership()
|
||||
setup_global_gitignore()
|
||||
|
||||
print("[post_install] Configuration complete!", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user