├── .github └── workflows │ ├── release.yaml │ ├── test.yaml │ └── validate.yaml ├── .gitignore ├── LICENSE ├── README.md ├── src └── claude-code │ ├── NOTES.md │ ├── README.md │ ├── devcontainer-feature.json │ ├── init-firewall.sh │ └── install.sh └── test └── claude-code ├── basic.sh ├── scenarios.json ├── test.sh └── with-firewall.sh /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "Release Dev Container Features" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | if: ${{ github.ref == 'refs/heads/main' }} 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | packages: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: "Publish Features" 21 | uses: devcontainers/action@v1 22 | with: 23 | publish-features: "true" 24 | base-path-to-features: "./src" 25 | generate-docs: "true" 26 | 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Create PR for Documentation 31 | id: push_image_info 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | set -e 36 | echo "Start." 37 | 38 | # Create new branch for documentation updates 39 | branch=automated-documentation-update-$GITHUB_RUN_ID 40 | echo "Creating branch: $branch" 41 | 42 | # Create branch via API (this will be automatically signed) 43 | gh api \ 44 | --method POST \ 45 | /repos/$GITHUB_REPOSITORY/git/refs \ 46 | -f ref="refs/heads/$branch" \ 47 | -f sha="$GITHUB_SHA" 48 | 49 | message='Automated documentation update [skip ci]' 50 | 51 | # Find all updated README.md files 52 | readmes=$(find . -name "README.md" -path "*/*/README.md") 53 | 54 | # Check if any README files were found 55 | if [ -z "$readmes" ]; then 56 | echo "No README files found to update" 57 | exit 0 58 | fi 59 | 60 | # Commit each README file via GitHub API (automatically signed) 61 | for readme in $readmes; do 62 | # Get the current file from the source branch to get its SHA 63 | file_path=${readme:2} # Remove leading ./ 64 | echo "Committing updates to $file_path" 65 | 66 | # Get current file SHA from the new branch 67 | file_sha=$(gh api \ 68 | --method GET \ 69 | /repos/$GITHUB_REPOSITORY/contents/$file_path?ref=main \ 70 | -q '.sha') 71 | 72 | echo "SHA for $file_path: $file_sha" 73 | 74 | # Use GitHub API to commit the file (automatically signed) 75 | gh api \ 76 | --method PUT \ 77 | /repos/$GITHUB_REPOSITORY/contents/$file_path \ 78 | -f message="$message" \ 79 | -f content="$(base64 -i $readme)" \ 80 | -f sha="$file_sha" \ 81 | -f branch="$branch" \ 82 | || echo "No changes to commit for $file_path" 83 | done 84 | 85 | # Create PR 86 | gh pr create --title "$message" --body "$message" --head "$branch" --base "main" || echo "No changes to create PR for" 87 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "CI - Test Features" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test-autogenerated: 11 | runs-on: ubuntu-latest 12 | continue-on-error: true 13 | strategy: 14 | matrix: 15 | features: 16 | - claude-code 17 | baseImage: 18 | - debian:latest 19 | - ubuntu:latest 20 | - mcr.microsoft.com/devcontainers/base:ubuntu 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: "Install latest devcontainer CLI" 25 | run: npm install -g @devcontainers/cli 26 | 27 | - name: "Generating tests for '${{ matrix.features }}' against '${{ matrix.baseImage }}'" 28 | run: devcontainer features test --skip-scenarios -f ${{ matrix.features }} -i ${{ matrix.baseImage }} . 29 | 30 | test-scenarios: 31 | runs-on: ubuntu-latest 32 | continue-on-error: true 33 | strategy: 34 | matrix: 35 | features: 36 | - claude-code 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: "Install latest devcontainer CLI" 41 | run: npm install -g @devcontainers/cli 42 | 43 | - name: "Generating tests for '${{ matrix.features }}' scenarios" 44 | run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated --skip-duplicated . 45 | 46 | test-global: 47 | runs-on: ubuntu-latest 48 | continue-on-error: true 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - name: "Install latest devcontainer CLI" 53 | run: npm install -g @devcontainers/cli 54 | 55 | - name: "Testing global scenarios" 56 | run: devcontainer features test --global-scenarios-only . -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: "Validate devcontainer-feature.json files" 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | 6 | jobs: 7 | validate: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: "Validate devcontainer-feature.json files" 13 | uses: devcontainers/action@v1 14 | with: 15 | validate-only: "true" 16 | base-path-to-features: "./src" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Development containers 6 | .devcontainer/ 7 | 8 | # Build artifacts 9 | .build/ 10 | 11 | # System files 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Log files 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Editor specific files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea/ 25 | *.swp 26 | *.swo 27 | *~ 28 | 29 | # Test artifacts 30 | test-results/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Anthropic PBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dev Container Features 2 | 3 | This repository contains [Dev Container Features](https://containers.dev/implementors/features/), including one that installs the Claude Code CLI. 4 | 5 | ## Contents 6 | 7 | - `src/claude-code`: The Claude Code CLI feature 8 | - `test`: Automated tests for the feature 9 | 10 | ## Usage 11 | 12 | To use this feature in your devcontainer, add it to your `devcontainer.json` file: 13 | 14 | ```json 15 | "features": { 16 | "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {} 17 | } 18 | ``` 19 | 20 | ## Requirements 21 | 22 | The feature automatically depends on Node.js and will install it if not already present. 23 | 24 | ## Building and Testing 25 | 26 | You can build and test the feature using the [dev container CLI](https://github.com/devcontainers/cli): 27 | 28 | ```bash 29 | # Test the feature 30 | devcontainer features test -f claude-code . 31 | 32 | # Publish the feature 33 | devcontainer feature publish -n anthropics/devcontainer-features . 34 | ``` 35 | 36 | ## License 37 | 38 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /src/claude-code/NOTES.md: -------------------------------------------------------------------------------- 1 | # Using Claude Code in devcontainers 2 | 3 | ## Requirements 4 | 5 | This feature requires Node.js and npm to be available in the container. You need to either: 6 | 7 | 1. Use a base container image that includes Node.js, or 8 | 2. Add the Node.js feature to your devcontainer.json 9 | 3. Let this feature attempt to install Node.js automatically (best-effort, works on Debian/Ubuntu, Alpine, Fedora, RHEL, and CentOS) 10 | 11 | Note: When auto-installing Node.js, a compatible LTS version (Node.js 18.x) will be used. 12 | 13 | ## Recommended configuration 14 | 15 | For most setups, we recommend explicitly adding both features: 16 | 17 | ```json 18 | "features": { 19 | "ghcr.io/devcontainers/features/node:1": {}, 20 | "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} 21 | } 22 | ``` 23 | 24 | ## Using with containers that already have Node.js 25 | 26 | If your container already has Node.js installed (for example, a container based on a Node.js image or one using nvm), you can use the Claude Code feature directly without adding the Node.js feature: 27 | 28 | ```json 29 | "features": { 30 | "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} 31 | } 32 | ``` 33 | 34 | ## Using with nvm 35 | 36 | When using with containers that have nvm pre-installed, you can use the Claude Code feature directly, and it will use the existing Node.js installation. 37 | 38 | ## Optional Network Firewall 39 | 40 | This feature includes a network firewall script that you can optionally enable to restrict outbound traffic to only essential services (GitHub, npm registry, Anthropic API, etc.). This improves security by limiting the container's network access. 41 | 42 | The firewall script is installed but not enabled by default. To enable the firewall, add these to your devcontainer.json: 43 | 44 | ```json 45 | "runArgs": [ 46 | "--cap-add=NET_ADMIN", 47 | "--cap-add=NET_RAW" 48 | ], 49 | "postCreateCommand": "sudo /usr/local/bin/init-firewall.sh" 50 | ``` 51 | 52 | The firewall will be initialized when the container starts, blocking all outbound connections except to essential services. The allowed services include: 53 | 54 | - GitHub API, Git, and Web services 55 | - npm registry 56 | - Anthropic API 57 | - Sentry.io 58 | - Statsig services 59 | 60 | All other outbound connections will be blocked, providing an additional layer of security for your development environment. 61 | 62 | ### How the Firewall Works 63 | 64 | The firewall uses iptables and ipset to: 65 | 66 | 1. Create a whitelist of allowed domains and IP addresses 67 | 2. Allow all established connections and responses 68 | 3. Allow outbound DNS and SSH 69 | 4. Block all other outbound connections 70 | 71 | The script automatically resolves and adds the IP addresses for essential services to the whitelist. If you need to add additional domains to the allowed list, you can modify the firewall script at `/usr/local/bin/init-firewall.sh`. 72 | -------------------------------------------------------------------------------- /src/claude-code/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Claude Code CLI (claude-code) 3 | 4 | Installs the Claude Code CLI globally 5 | 6 | ## Example Usage 7 | 8 | ```json 9 | "features": { 10 | "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} 11 | } 12 | ``` 13 | 14 | ## Options 15 | 16 | | Options Id | Description | Type | Default Value | 17 | |-----|-----|-----|-----| 18 | 19 | 20 | ## Customizations 21 | 22 | ### VS Code Extensions 23 | 24 | - `anthropic.claude-code` 25 | 26 | # Using Claude Code in devcontainers 27 | 28 | ## Requirements 29 | 30 | This feature requires Node.js and npm to be available in the container. You need to either: 31 | 32 | 1. Use a base container image that includes Node.js, or 33 | 2. Add the Node.js feature to your devcontainer.json 34 | 3. Let this feature attempt to install Node.js automatically (best-effort, works on Debian/Ubuntu, Alpine, Fedora, RHEL, and CentOS) 35 | 36 | Note: When auto-installing Node.js, a compatible LTS version (Node.js 18.x) will be used. 37 | 38 | ## Recommended configuration 39 | 40 | For most setups, we recommend explicitly adding both features: 41 | 42 | ```json 43 | "features": { 44 | "ghcr.io/devcontainers/features/node:1": {}, 45 | "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} 46 | } 47 | ``` 48 | 49 | ## Using with containers that already have Node.js 50 | 51 | If your container already has Node.js installed (for example, a container based on a Node.js image or one using nvm), you can use the Claude Code feature directly without adding the Node.js feature: 52 | 53 | ```json 54 | "features": { 55 | "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} 56 | } 57 | ``` 58 | 59 | ## Using with nvm 60 | 61 | When using with containers that have nvm pre-installed, you can use the Claude Code feature directly, and it will use the existing Node.js installation. 62 | 63 | ## Optional Network Firewall 64 | 65 | This feature includes a network firewall script that you can optionally enable to restrict outbound traffic to only essential services (GitHub, npm registry, Anthropic API, etc.). This improves security by limiting the container's network access. 66 | 67 | The firewall script is installed but not enabled by default. To enable the firewall, add these to your devcontainer.json: 68 | 69 | ```json 70 | "runArgs": [ 71 | "--cap-add=NET_ADMIN", 72 | "--cap-add=NET_RAW" 73 | ], 74 | "postCreateCommand": "sudo /usr/local/bin/init-firewall.sh" 75 | ``` 76 | 77 | The firewall will be initialized when the container starts, blocking all outbound connections except to essential services. The allowed services include: 78 | 79 | - GitHub API, Git, and Web services 80 | - npm registry 81 | - Anthropic API 82 | - Sentry.io 83 | - Statsig services 84 | 85 | All other outbound connections will be blocked, providing an additional layer of security for your development environment. 86 | 87 | ### How the Firewall Works 88 | 89 | The firewall uses iptables and ipset to: 90 | 91 | 1. Create a whitelist of allowed domains and IP addresses 92 | 2. Allow all established connections and responses 93 | 3. Allow outbound DNS and SSH 94 | 4. Block all other outbound connections 95 | 96 | The script automatically resolves and adds the IP addresses for essential services to the whitelist. If you need to add additional domains to the allowed list, you can modify the firewall script at `/usr/local/bin/init-firewall.sh`. 97 | 98 | 99 | --- 100 | 101 | _Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/anthropics/devcontainer-features/blob/main/src/claude-code/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ 102 | -------------------------------------------------------------------------------- /src/claude-code/devcontainer-feature.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Claude Code CLI", 3 | "id": "claude-code", 4 | "version": "1.0.5", 5 | "description": "Installs the Claude Code CLI globally", 6 | "options": {}, 7 | "documentationURL": "https://github.com/anthropics/devcontainer-features/tree/main/src/claude-code", 8 | "licenseURL": "https://github.com/anthropics/devcontainer-features/blob/main/LICENSE", 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "anthropic.claude-code" 13 | ] 14 | } 15 | }, 16 | "installsAfter": [ 17 | "ghcr.io/devcontainers/features/node" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/claude-code/init-firewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail # Exit on error, undefined vars, and pipeline failures 3 | IFS=$'\n\t' # Stricter word splitting 4 | 5 | # Flush existing rules and delete existing ipsets 6 | iptables -F 7 | iptables -X 8 | iptables -t nat -F 9 | iptables -t nat -X 10 | iptables -t mangle -F 11 | iptables -t mangle -X 12 | ipset destroy allowed-domains 2>/dev/null || true 13 | 14 | # First allow DNS and localhost before any restrictions 15 | # Allow outbound DNS 16 | iptables -A OUTPUT -p udp --dport 53 -j ACCEPT 17 | # Allow inbound DNS responses 18 | iptables -A INPUT -p udp --sport 53 -j ACCEPT 19 | # Allow outbound SSH 20 | iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT 21 | # Allow inbound SSH responses 22 | iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT 23 | # Allow localhost 24 | iptables -A INPUT -i lo -j ACCEPT 25 | iptables -A OUTPUT -o lo -j ACCEPT 26 | 27 | # Create ipset with CIDR support 28 | ipset create allowed-domains hash:net 29 | 30 | # Fetch GitHub meta information and aggregate + add their IP ranges 31 | echo "Fetching GitHub IP ranges..." 32 | gh_ranges=$(curl -s https://api.github.com/meta) 33 | if [ -z "$gh_ranges" ]; then 34 | echo "ERROR: Failed to fetch GitHub IP ranges" 35 | exit 1 36 | fi 37 | 38 | if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then 39 | echo "ERROR: GitHub API response missing required fields" 40 | exit 1 41 | fi 42 | 43 | echo "Processing GitHub IPs..." 44 | while read -r cidr; do 45 | if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then 46 | echo "ERROR: Invalid CIDR range from GitHub meta: $cidr" 47 | exit 1 48 | fi 49 | echo "Adding GitHub range $cidr" 50 | ipset add allowed-domains "$cidr" 51 | done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q) 52 | 53 | # Resolve and add other allowed domains 54 | for domain in \ 55 | "registry.npmjs.org" \ 56 | "api.anthropic.com" \ 57 | "sentry.io" \ 58 | "statsig.anthropic.com" \ 59 | "statsig.com"; do 60 | echo "Resolving $domain..." 61 | ips=$(dig +short A "$domain") 62 | if [ -z "$ips" ]; then 63 | echo "ERROR: Failed to resolve $domain" 64 | exit 1 65 | fi 66 | 67 | while read -r ip; do 68 | if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 69 | echo "ERROR: Invalid IP from DNS for $domain: $ip" 70 | exit 1 71 | fi 72 | echo "Adding $ip for $domain" 73 | ipset add allowed-domains "$ip" 74 | done < <(echo "$ips") 75 | done 76 | 77 | # Get host IP from default route 78 | HOST_IP=$(ip route | grep default | cut -d" " -f3) 79 | if [ -z "$HOST_IP" ]; then 80 | echo "ERROR: Failed to detect host IP" 81 | exit 1 82 | fi 83 | 84 | HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") 85 | echo "Host network detected as: $HOST_NETWORK" 86 | 87 | # Set up remaining iptables rules 88 | iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT 89 | iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT 90 | 91 | # Set default policies to DROP first 92 | # Set default policies to DROP first 93 | iptables -P INPUT DROP 94 | iptables -P FORWARD DROP 95 | iptables -P OUTPUT DROP 96 | 97 | # First allow established connections for already approved traffic 98 | iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 99 | iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 100 | 101 | # Then allow only specific outbound traffic to allowed domains 102 | iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT 103 | 104 | echo "Firewall configuration complete" 105 | echo "Verifying firewall rules..." 106 | if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then 107 | echo "ERROR: Firewall verification failed - was able to reach https://example.com" 108 | exit 1 109 | else 110 | echo "Firewall verification passed - unable to reach https://example.com as expected" 111 | fi 112 | 113 | # Verify GitHub API access 114 | if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then 115 | echo "ERROR: Firewall verification failed - unable to reach https://api.github.com" 116 | exit 1 117 | else 118 | echo "Firewall verification passed - able to reach https://api.github.com as expected" 119 | fi 120 | -------------------------------------------------------------------------------- /src/claude-code/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | # Function to detect the package manager and OS type 5 | detect_package_manager() { 6 | for pm in apt-get apk dnf yum; do 7 | if command -v $pm >/dev/null; then 8 | case $pm in 9 | apt-get) echo "apt" ;; 10 | *) echo "$pm" ;; 11 | esac 12 | return 0 13 | fi 14 | done 15 | echo "unknown" 16 | return 1 17 | } 18 | 19 | # Function to install packages using the appropriate package manager 20 | install_packages() { 21 | local pkg_manager="$1" 22 | shift 23 | local packages="$@" 24 | 25 | case "$pkg_manager" in 26 | apt) 27 | apt-get update 28 | apt-get install -y $packages 29 | ;; 30 | apk) 31 | apk add --no-cache $packages 32 | ;; 33 | dnf|yum) 34 | $pkg_manager install -y $packages 35 | ;; 36 | *) 37 | echo "WARNING: Unsupported package manager. Cannot install packages: $packages" 38 | return 1 39 | ;; 40 | esac 41 | 42 | return 0 43 | } 44 | 45 | # Function to install Node.js 46 | install_nodejs() { 47 | local pkg_manager="$1" 48 | 49 | echo "Installing Node.js using $pkg_manager..." 50 | 51 | case "$pkg_manager" in 52 | apt) 53 | # Debian/Ubuntu - install more recent Node.js LTS 54 | install_packages apt "ca-certificates curl gnupg" 55 | mkdir -p /etc/apt/keyrings 56 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 57 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list 58 | apt-get update 59 | apt-get install -y nodejs 60 | ;; 61 | apk) 62 | # Alpine 63 | install_packages apk "nodejs npm" 64 | ;; 65 | dnf) 66 | # Fedora/RHEL 67 | install_packages dnf "nodejs npm" 68 | ;; 69 | yum) 70 | # CentOS/RHEL 71 | curl -sL https://rpm.nodesource.com/setup_18.x | bash - 72 | yum install -y nodejs 73 | ;; 74 | *) 75 | echo "ERROR: Unsupported package manager for Node.js installation" 76 | return 1 77 | ;; 78 | esac 79 | 80 | # Verify installation 81 | if command -v node >/dev/null && command -v npm >/dev/null; then 82 | echo "Successfully installed Node.js and npm" 83 | return 0 84 | else 85 | echo "Failed to install Node.js and npm" 86 | return 1 87 | fi 88 | } 89 | 90 | # Map of package manager to required firewall packages 91 | get_firewall_packages() { 92 | local pkg_manager="$1" 93 | 94 | case "$pkg_manager" in 95 | apt) echo "iptables ipset dnsutils jq curl aggregate" ;; 96 | apk) echo "iptables ipset bind-tools jq curl aggregate" ;; 97 | dnf|yum) echo "iptables ipset bind-utils jq curl aggregate" ;; 98 | *) echo "" ;; 99 | esac 100 | } 101 | 102 | # Function to install firewall packages 103 | install_firewall_packages() { 104 | local pkg_manager="$1" 105 | local packages=$(get_firewall_packages "$pkg_manager") 106 | 107 | if [ -z "$packages" ]; then 108 | echo "WARNING: Could not determine firewall packages for this system type" 109 | return 1 110 | fi 111 | 112 | echo "Installing firewall packages: $packages" 113 | install_packages "$pkg_manager" $packages 114 | } 115 | 116 | # Function to set up firewall script 117 | setup_firewall_script() { 118 | local script_path="/usr/local/bin/init-firewall.sh" 119 | 120 | echo "Setting up firewall initialization script..." 121 | 122 | # Create destination directory and copy the script 123 | mkdir -p /usr/local/bin 124 | cp "$(dirname "$0")/init-firewall.sh" "$script_path" 125 | chmod +x "$script_path" 126 | 127 | cat </dev/null; then 151 | echo "Claude Code CLI installed successfully!" 152 | claude --version 153 | return 0 154 | else 155 | echo "ERROR: Claude Code CLI installation failed!" 156 | return 1 157 | fi 158 | } 159 | 160 | # Print error message about requiring Node.js feature 161 | print_nodejs_requirement() { 162 | cat </dev/null || ! command -v npm >/dev/null; then 190 | echo "Node.js or npm not found, attempting to install automatically..." 191 | install_nodejs "$PKG_MANAGER" || print_nodejs_requirement 192 | fi 193 | 194 | # Install Claude Code CLI 195 | install_claude_code || exit 1 196 | 197 | # Always set up the firewall script, but don't execute it 198 | setup_firewall_script 199 | } 200 | 201 | # Execute main function 202 | main -------------------------------------------------------------------------------- /test/claude-code/basic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Test if Claude CLI is installed 6 | if ! command -v claude &> /dev/null; then 7 | echo "claude command not found" 8 | exit 1 9 | fi 10 | 11 | # Test version output 12 | if ! claude -v | grep -q "Claude Code"; then 13 | echo "claude version check failed" 14 | exit 1 15 | fi 16 | 17 | echo "Claude CLI installation test passed!" 18 | exit 0 -------------------------------------------------------------------------------- /test/claude-code/scenarios.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": { 3 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 4 | "features": { 5 | "ghcr.io/devcontainers/features/node:1": {}, 6 | "claude-code": {} 7 | } 8 | }, 9 | "with-firewall": { 10 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 11 | "features": { 12 | "ghcr.io/devcontainers/features/node:1": {}, 13 | "claude-code": {} 14 | }, 15 | "runArgs": [ 16 | "--cap-add=NET_ADMIN", 17 | "--cap-add=NET_RAW" 18 | ], 19 | "postCreateCommand": "sudo /usr/local/bin/init-firewall.sh" 20 | } 21 | } -------------------------------------------------------------------------------- /test/claude-code/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Optional: Import test library 6 | source dev-container-features-test-lib 7 | 8 | # Feature-specific tests 9 | check "node version" node --version 10 | check "npm version" npm --version 11 | check "claude cli installed" command -v claude 12 | check "claude version" claude --version 13 | 14 | # The firewall script should always exist now 15 | check "firewall script exists" test -f /usr/local/bin/init-firewall.sh 16 | check "firewall script is executable" test -x /usr/local/bin/init-firewall.sh 17 | 18 | # Report results 19 | reportResults -------------------------------------------------------------------------------- /test/claude-code/with-firewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Optional: Import test library 6 | source dev-container-features-test-lib 7 | 8 | # Verify firewall script exists and is executable 9 | if [ ! -f /usr/local/bin/init-firewall.sh ]; then 10 | echo "ERROR: Firewall script not found at /usr/local/bin/init-firewall.sh" 11 | check "firewall script exists" false 12 | reportResults 13 | exit 1 14 | fi 15 | 16 | if [ ! -x /usr/local/bin/init-firewall.sh ]; then 17 | echo "ERROR: Firewall script is not executable" 18 | check "firewall script is executable" false 19 | reportResults 20 | exit 1 21 | fi 22 | 23 | # Check if we can reach GitHub API (should be allowed) 24 | if curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then 25 | check "can reach GitHub API" true 26 | else 27 | echo "ERROR: Cannot reach GitHub API, which should be allowed" 28 | check "can reach GitHub API" false 29 | fi 30 | 31 | # Check if we can reach example.com (should be blocked) 32 | if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then 33 | echo "ERROR: Can reach example.com, which should be blocked" 34 | check "example.com is blocked" false 35 | else 36 | check "example.com is blocked" true 37 | fi 38 | 39 | # Report results 40 | reportResults 41 | --------------------------------------------------------------------------------