@jgamaraalv/ts-dev-kit 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,14 @@
5
5
  "url": "https://github.com/jgamaraalv"
6
6
  },
7
7
  "metadata": {
8
- "description": "15 agents and 21 skills for TypeScript fullstack development with Claude Code"
8
+ "description": "15 agents and 22 skills for TypeScript fullstack development with Claude Code"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "ts-dev-kit",
13
13
  "source": "./",
14
- "description": "15 specialized agents and 21 skills for TypeScript fullstack development",
15
- "version": "3.0.1",
14
+ "description": "15 specialized agents and 22 skills for TypeScript fullstack development",
15
+ "version": "3.1.1",
16
16
  "author": {
17
17
  "name": "jgamaraalv"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ts-dev-kit",
3
- "version": "3.0.1",
4
- "description": "15 specialized agents and 21 skills for TypeScript fullstack development with Fastify, Next.js, PostgreSQL, Redis, and more.",
3
+ "version": "3.1.1",
4
+ "description": "15 specialized agents and 22 skills for TypeScript fullstack development with Fastify, Next.js, PostgreSQL, Redis, and more.",
5
5
  "author": {
6
6
  "name": "jgamaraalv",
7
7
  "url": "https://github.com/jgamaraalv"
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.1.1] - 2026-02-27
9
+
10
+ ### Fixed
11
+
12
+ - `/yolo` firewall script: add resilient fallback for Docker Desktop environments where `ipset` kernel module is unavailable — falls back to iptables-only rules, `getent` DNS resolution, and direct GitHub domain resolution; full diagnostic log at `/tmp/firewall-init.log`
13
+
14
+ ## [3.1.0] - 2026-02-27
15
+
16
+ ### Added
17
+
18
+ - `/yolo` skill: devcontainer setup and launch workflow for running Claude Code with `--dangerously-skip-permissions` in a secure, sandboxed environment — detects existing devcontainer, checks Docker status, scaffolds the Claude Code reference `.devcontainer/` (Dockerfile, firewall, config), starts Docker if needed, and opens VS Code to launch the container
19
+ - Dynamic context injection to `/yolo`: project root, devcontainer presence, Docker daemon status, Docker and VS Code installation are pre-injected before Claude receives the prompt
20
+
8
21
  ## [3.0.1] - 2026-02-27
9
22
 
10
23
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ts-dev-kit
2
2
 
3
- > 15 specialized agents + 21 curated skills for TypeScript fullstack development
3
+ > 15 specialized agents + 22 curated skills for TypeScript fullstack development
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@jgamaraalv/ts-dev-kit)](https://www.npmjs.com/package/@jgamaraalv/ts-dev-kit)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
@@ -30,7 +30,7 @@
30
30
  | typescript-pro | Generics, type inference, conditional types |
31
31
  | ux-optimizer | User flows, form UX, friction reduction |
32
32
 
33
- ### Skills (21)
33
+ ### Skills (22)
34
34
 
35
35
  | Skill | Slug | Domain |
36
36
  | ---------------------- | ------------------------- | ------------------------------------------------- |
@@ -55,6 +55,7 @@
55
55
  | TanStack Query | `/tanstack-query` | React Query v5, caching, SSR/hydration |
56
56
  | TypeScript Conventions | `/typescript-conventions` | Strict config, patterns, best practices |
57
57
  | UI/UX Guidelines | `/ui-ux-guidelines` | Accessibility, layout, forms |
58
+ | Yolo | `/yolo` | Devcontainer setup for autonomous Claude Code |
58
59
 
59
60
  ---
60
61
 
@@ -206,6 +207,7 @@ Several skills use the `!`command`` syntax to pre-inject live data before Claude
206
207
  | `/conventional-commits` | Staged diff summary, full staged diff, last 8 commit messages |
207
208
  | `/debug` | Last 10 git commits, working tree status |
208
209
  | `/codebase-adapter` | Working directory, lockfile, installed agents, MCP servers, `package.json` |
210
+ | `/yolo` | Project root, devcontainer presence, Docker status, VS Code installation |
209
211
 
210
212
  ---
211
213
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jgamaraalv/ts-dev-kit",
3
- "version": "3.0.1",
4
- "description": "Claude Code plugin: 15 agents + 21 skills for TypeScript fullstack development",
3
+ "version": "3.1.1",
4
+ "description": "Claude Code plugin: 15 agents + 22 skills for TypeScript fullstack development",
5
5
  "author": "jgamaraalv",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -22,6 +22,10 @@ allowed-tools: Bash(git *)
22
22
  - "Debug the notification queue — jobs are stuck"
23
23
  </trigger_examples>
24
24
 
25
+ <task>
26
+ $ARGUMENTS
27
+ </task>
28
+
25
29
  <workflow>
26
30
  Follow each phase in order. Each one feeds the next.
27
31
 
@@ -233,7 +237,3 @@ When complete, produce a debug report using the template in [template.md](templa
233
237
 
234
238
  Do not add explanations, caveats, or follow-up suggestions unless the user asks.
235
239
  </output>
236
-
237
- <task>
238
- $ARGUMENTS
239
- </task>
@@ -0,0 +1,331 @@
1
+ ---
2
+ name: yolo
3
+ description: "Sets up and launches a Claude Code devcontainer with --dangerously-skip-permissions for fully autonomous, unattended operation inside a sandboxed environment. Use when: (1) user wants to run Claude Code without permission prompts, (2) setting up a secure development container for autonomous coding, (3) user says 'yolo', 'yolo mode', 'run without permissions', 'autonomous mode', 'skip permissions safely', or 'devcontainer setup'."
4
+ argument-hint: "[optional: path to project root — defaults to current directory]"
5
+ allowed-tools: Bash(docker *), Bash(ls *), Bash(cat *), Bash(cp *), Bash(mkdir *), Bash(which *), Bash(open *), Bash(code *), Bash(command *)
6
+ ---
7
+
8
+ <live_context>
9
+ **Project root:**
10
+ !`pwd`
11
+
12
+ **Devcontainer present:**
13
+ !`ls -la .devcontainer/devcontainer.json 2>/dev/null && echo "YES" || echo "NO"`
14
+
15
+ **Docker daemon status:**
16
+ !`docker info --format '{{.ServerVersion}}' 2>/dev/null && echo "RUNNING" || echo "NOT RUNNING"`
17
+
18
+ **Docker Desktop installed:**
19
+ !`command -v docker 2>/dev/null || echo "NOT FOUND"`
20
+
21
+ **VS Code installed:**
22
+ !`command -v code 2>/dev/null || echo "NOT FOUND"`
23
+ </live_context>
24
+
25
+ <trigger_examples>
26
+ - "yolo"
27
+ - "Enter yolo mode"
28
+ - "Set up a devcontainer so Claude can run autonomously"
29
+ - "I want to run Claude Code without permission prompts"
30
+ - "Skip permissions safely with a devcontainer"
31
+ - "Set up autonomous mode"
32
+ </trigger_examples>
33
+
34
+ <system>
35
+ You are a devcontainer setup specialist. Your goal is to get the user into a secure, sandboxed devcontainer running `claude --dangerously-skip-permissions` as quickly as possible. You follow a strict decision tree and never skip safety checks.
36
+
37
+ **IMPORTANT SECURITY NOTE:** While the devcontainer provides substantial protections (network firewall, isolation), it is NOT immune to all attacks. Only use devcontainers with **trusted repositories**. The `--dangerously-skip-permissions` flag gives Claude full access to everything inside the container, including credentials mounted into it. Always inform the user of this trade-off.
38
+ </system>
39
+
40
+ <task>
41
+ $ARGUMENTS
42
+ </task>
43
+
44
+ <workflow>
45
+ Follow this decision tree strictly. Each phase gates the next.
46
+
47
+ ```
48
+ START
49
+
50
+ ├─ Phase 1: Detect devcontainer
51
+ │ ├─ EXISTS → Phase 2
52
+ │ └─ MISSING → Phase 5 (install) → Phase 2
53
+
54
+ ├─ Phase 2: Check Docker
55
+ │ ├─ RUNNING → Phase 3
56
+ │ └─ NOT RUNNING → Phase 4 (start Docker) → Phase 3
57
+
58
+ ├─ Phase 3: Launch devcontainer
59
+ │ └─ claude --dangerously-skip-permissions
60
+
61
+ ├─ Phase 4: Start Docker
62
+ │ └─ Start Docker Desktop/daemon → Phase 3
63
+
64
+ └─ Phase 5: Install devcontainer
65
+ └─ Scaffold .devcontainer/ → Phase 2
66
+ ```
67
+
68
+ <phase_1_detect>
69
+
70
+ ## Phase 1 — Detect devcontainer
71
+
72
+ Check whether `.devcontainer/devcontainer.json` exists in the project root.
73
+
74
+ 1. Read the `<live_context>` above for the pre-injected detection result.
75
+ 2. If **YES** — announce to the user and proceed to Phase 2:
76
+
77
+ > **DEVCONTAINER DETECTED** — `.devcontainer/` found. Checking Docker status...
78
+
79
+ 3. If **NO** — announce and proceed to Phase 5:
80
+
81
+ > **NO DEVCONTAINER FOUND** — I'll set one up using the Claude Code reference implementation.
82
+
83
+ </phase_1_detect>
84
+
85
+ <phase_2_check_docker>
86
+
87
+ ## Phase 2 — Check Docker
88
+
89
+ Verify the Docker daemon is running and accessible.
90
+
91
+ 1. Read the `<live_context>` above for the pre-injected Docker status.
92
+ 2. If `docker` command is **NOT FOUND**:
93
+
94
+ > **DOCKER NOT INSTALLED** — Docker Desktop is required for devcontainers. Please install it from https://www.docker.com/products/docker-desktop/ and re-run `/yolo`.
95
+
96
+ Stop here. Do not proceed.
97
+
98
+ 3. If Docker is **RUNNING** — proceed to Phase 3:
99
+
100
+ > **DOCKER RUNNING** (version X.X.X) — Ready to launch devcontainer.
101
+
102
+ 4. If Docker is **NOT RUNNING** — proceed to Phase 4.
103
+
104
+ </phase_2_check_docker>
105
+
106
+ <phase_3_launch>
107
+
108
+ ## Phase 3 — Launch devcontainer
109
+
110
+ Open the project in VS Code with the devcontainer.
111
+
112
+ **Prerequisites check:**
113
+
114
+ 1. Verify VS Code is installed (`command -v code`).
115
+ 2. If VS Code is not installed, inform the user:
116
+
117
+ > **VS Code is required** for devcontainer support. Install it from https://code.visualstudio.com/ and re-run `/yolo`.
118
+
119
+ Stop here. Do not proceed.
120
+
121
+ 3. Ensure the Dev Containers extension is installed:
122
+
123
+ ```bash
124
+ code --install-extension ms-vscode-remote.remote-containers
125
+ ```
126
+
127
+ This is idempotent — if already installed, VS Code simply reports it and moves on.
128
+
129
+ **Launch sequence:**
130
+
131
+ 1. Open the project in VS Code:
132
+
133
+ ```bash
134
+ code .
135
+ ```
136
+
137
+ 2. Instruct the user:
138
+
139
+ > **LAUNCHING DEVCONTAINER**
140
+ >
141
+ > VS Code is opening your project. Follow these steps:
142
+ >
143
+ > 1. VS Code should prompt **"Reopen in Container"** — click it.
144
+ > - If no prompt appears: open Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) → type **"Dev Containers: Reopen in Container"** → select it.
145
+ > 2. Wait for the container to build (first time takes 2-5 minutes as it installs Node.js 20, Claude Code, ZSH, and configures the firewall).
146
+ > 3. Once inside the container, open a terminal and run:
147
+ > ```
148
+ > claude --dangerously-skip-permissions
149
+ > ```
150
+ >
151
+ > **What's happening under the hood:**
152
+ >
153
+ > - The container runs Node.js 20 with Claude Code pre-installed
154
+ > - A firewall restricts outbound traffic to: npm registry, GitHub, Claude API, Sentry, and VS Code marketplace only
155
+ > - All other internet access is blocked (verified on startup)
156
+ > - Your project is mounted at `/workspace`
157
+ > - Shell history and Claude config persist between container restarts
158
+ >
159
+ > **Security trade-off:** `--dangerously-skip-permissions` means Claude can execute ANY command inside the container without asking. The firewall and container isolation are your safety net. Only use this with trusted code.
160
+
161
+ </phase_3_launch>
162
+
163
+ <phase_4_start_docker>
164
+
165
+ ## Phase 4 — Start Docker
166
+
167
+ The Docker daemon is installed but not running.
168
+
169
+ **macOS:**
170
+
171
+ ```bash
172
+ open -a Docker
173
+ ```
174
+
175
+ **Linux:**
176
+
177
+ ```bash
178
+ sudo systemctl start docker
179
+ ```
180
+
181
+ After issuing the start command:
182
+
183
+ 1. Wait for Docker to become ready (poll up to 30 seconds):
184
+
185
+ ```bash
186
+ for i in $(seq 1 15); do
187
+ docker info --format '{{.ServerVersion}}' 2>/dev/null && break
188
+ sleep 2
189
+ done
190
+ ```
191
+
192
+ 2. Re-verify Docker is running:
193
+
194
+ ```bash
195
+ docker info --format '{{.ServerVersion}}' 2>/dev/null
196
+ ```
197
+
198
+ 3. If Docker started successfully — proceed to Phase 3:
199
+
200
+ > **DOCKER STARTED** — Docker daemon is now running. Launching devcontainer...
201
+
202
+ 4. If Docker failed to start after 30 seconds:
203
+
204
+ > **DOCKER FAILED TO START** — Please start Docker Desktop manually and re-run `/yolo`.
205
+
206
+ Stop here.
207
+
208
+ </phase_4_start_docker>
209
+
210
+ <phase_5_install_devcontainer>
211
+
212
+ ## Phase 5 — Install devcontainer
213
+
214
+ Scaffold the `.devcontainer/` directory with the Claude Code reference implementation.
215
+
216
+ ### Step 1 — Create directory
217
+
218
+ ```bash
219
+ mkdir -p .devcontainer
220
+ ```
221
+
222
+ ### Step 2 — Create `devcontainer.json`
223
+
224
+ Write the file using the reference config. See [references/devcontainer-json.md](references/devcontainer-json.md) for the full file content.
225
+
226
+ Key settings:
227
+
228
+ - Name: `"Claude Code Sandbox"`
229
+ - Dockerfile build with Node.js 20
230
+ - `NET_ADMIN` + `NET_RAW` capabilities (required for firewall)
231
+ - Claude Code VS Code extension pre-installed
232
+ - Volume mounts for bash history and Claude config persistence
233
+ - Firewall initialization via `postStartCommand`
234
+ - Workspace mounted at `/workspace`
235
+
236
+ ### Step 3 — Create `Dockerfile`
237
+
238
+ Write the file using the reference Dockerfile. See [references/dockerfile.md](references/dockerfile.md) for the full file content.
239
+
240
+ Key features:
241
+
242
+ - Base: `node:20`
243
+ - Installs: git, ZSH, fzf, gh CLI, iptables/ipset, nano, vim
244
+ - Installs `@anthropic-ai/claude-code` globally
245
+ - Sets up non-root `node` user
246
+ - Configures ZSH with Powerlevel10k theme
247
+ - Copies and configures firewall script with sudo access
248
+
249
+ ### Step 4 — Create `init-firewall.sh`
250
+
251
+ Write the file using the reference firewall script. See [references/init-firewall.sh.md](references/init-firewall.sh.md) for the full file content.
252
+
253
+ Key security features:
254
+
255
+ - Default-deny policy (DROP all INPUT, OUTPUT, FORWARD)
256
+ - Whitelisted outbound only: npm registry, GitHub (dynamic IP fetch), Claude API, Sentry, StatsIG, VS Code marketplace
257
+ - DNS and SSH allowed
258
+ - Localhost and host network allowed
259
+ - Startup verification: confirms `example.com` is blocked and `api.github.com` is reachable
260
+ - Docker DNS rules preserved
261
+ - Falls back to iptables-only rules if `ipset` kernel module is unavailable (common on Docker Desktop for Mac)
262
+ - Full diagnostic log at `/tmp/firewall-init.log` for troubleshooting
263
+
264
+ ### Step 5 — Add `.devcontainer` to `.gitignore` (optional)
265
+
266
+ Check if `.gitignore` exists and whether `.devcontainer/` is already listed. If not, ask the user:
267
+
268
+ > **Should I add `.devcontainer/` to `.gitignore`?**
269
+ >
270
+ > - **Yes** — keep devcontainer config local to your machine
271
+ > - **No** — commit it so the whole team can use it (recommended for teams)
272
+
273
+ ### Step 6 — Verify installation
274
+
275
+ ```bash
276
+ ls -la .devcontainer/
277
+ cat .devcontainer/devcontainer.json | head -5
278
+ ```
279
+
280
+ Announce completion:
281
+
282
+ > **DEVCONTAINER INSTALLED** — `.devcontainer/` is ready with Dockerfile, firewall script, and configuration. Proceeding to check Docker...
283
+
284
+ Return to Phase 2.
285
+
286
+ </phase_5_install_devcontainer>
287
+
288
+ </workflow>
289
+
290
+ <customization_notes>
291
+
292
+ ## Customizing the devcontainer
293
+
294
+ After installation, the user may want to customize:
295
+
296
+ **Adding allowed domains to the firewall:**
297
+ Edit `.devcontainer/init-firewall.sh` and add domains to the `for domain in` loop:
298
+
299
+ ```bash
300
+ for domain in \
301
+ "registry.npmjs.org" \
302
+ "api.anthropic.com" \
303
+ "your-custom-domain.com" \ # <-- add here
304
+ ...
305
+ ```
306
+
307
+ **Adding VS Code extensions:**
308
+ Edit `.devcontainer/devcontainer.json` → `customizations.vscode.extensions[]`:
309
+
310
+ ```json
311
+ "extensions": [
312
+ "anthropic.claude-code",
313
+ "your-extension.id"
314
+ ]
315
+ ```
316
+
317
+ **Changing Node.js version:**
318
+ Edit `.devcontainer/Dockerfile` — change the `FROM` line:
319
+
320
+ ```dockerfile
321
+ FROM node:22
322
+ ```
323
+
324
+ **Adding project-specific dependencies:**
325
+ Add `RUN` commands to the Dockerfile before the `USER node` line:
326
+
327
+ ```dockerfile
328
+ RUN apt-get update && apt-get install -y your-package
329
+ ```
330
+
331
+ </customization_notes>
@@ -0,0 +1,74 @@
1
+ # devcontainer.json — Reference
2
+
3
+ Write this file to `.devcontainer/devcontainer.json`:
4
+
5
+ ```json
6
+ {
7
+ "name": "Claude Code Sandbox",
8
+ "build": {
9
+ "dockerfile": "Dockerfile",
10
+ "args": {
11
+ "TZ": "${localEnv:TZ:America/Los_Angeles}",
12
+ "CLAUDE_CODE_VERSION": "latest",
13
+ "GIT_DELTA_VERSION": "0.18.2",
14
+ "ZSH_IN_DOCKER_VERSION": "1.2.0"
15
+ }
16
+ },
17
+ "runArgs": [
18
+ "--cap-add=NET_ADMIN",
19
+ "--cap-add=NET_RAW"
20
+ ],
21
+ "customizations": {
22
+ "vscode": {
23
+ "extensions": [
24
+ "anthropic.claude-code",
25
+ "dbaeumer.vscode-eslint",
26
+ "esbenp.prettier-vscode",
27
+ "eamodio.gitlens"
28
+ ],
29
+ "settings": {
30
+ "editor.formatOnSave": true,
31
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
32
+ "editor.codeActionsOnSave": {
33
+ "source.fixAll.eslint": "explicit"
34
+ },
35
+ "terminal.integrated.defaultProfile.linux": "zsh",
36
+ "terminal.integrated.profiles.linux": {
37
+ "bash": {
38
+ "path": "bash",
39
+ "icon": "terminal-bash"
40
+ },
41
+ "zsh": {
42
+ "path": "zsh"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "remoteUser": "node",
49
+ "mounts": [
50
+ "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
51
+ "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume"
52
+ ],
53
+ "containerEnv": {
54
+ "NODE_OPTIONS": "--max-old-space-size=4096",
55
+ "CLAUDE_CONFIG_DIR": "/home/node/.claude",
56
+ "POWERLEVEL9K_DISABLE_GITSTATUS": "true"
57
+ },
58
+ "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
59
+ "workspaceFolder": "/workspace",
60
+ "postStartCommand": "sudo /usr/local/bin/init-firewall.sh 2>&1 | tee /tmp/firewall-init.log",
61
+ "waitFor": "postStartCommand"
62
+ }
63
+ ```
64
+
65
+ ## Key settings explained
66
+
67
+ | Setting | Purpose |
68
+ |---------|---------|
69
+ | `runArgs: --cap-add=NET_ADMIN,NET_RAW` | Required for the firewall script to configure iptables |
70
+ | `remoteUser: node` | Runs as non-root for security |
71
+ | `mounts` (bashhistory, config) | Persists shell history and Claude config between container restarts |
72
+ | `postStartCommand` | Initializes the firewall on every container start |
73
+ | `waitFor: postStartCommand` | Ensures firewall is active before VS Code connects |
74
+ | `workspaceMount` | Binds the local project into `/workspace` with delegated consistency for performance |
@@ -0,0 +1,108 @@
1
+ # Dockerfile — Reference
2
+
3
+ Write this file to `.devcontainer/Dockerfile`:
4
+
5
+ ```dockerfile
6
+ FROM node:20
7
+
8
+ ARG TZ
9
+ ENV TZ="$TZ"
10
+
11
+ ARG CLAUDE_CODE_VERSION=latest
12
+
13
+ # Install basic development tools and iptables/ipset
14
+ RUN apt-get update && apt-get install -y --no-install-recommends \
15
+ less \
16
+ git \
17
+ procps \
18
+ sudo \
19
+ fzf \
20
+ zsh \
21
+ man-db \
22
+ unzip \
23
+ gnupg2 \
24
+ gh \
25
+ iptables \
26
+ ipset \
27
+ iproute2 \
28
+ dnsutils \
29
+ aggregate \
30
+ jq \
31
+ nano \
32
+ vim \
33
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
34
+
35
+ # Ensure default node user has access to /usr/local/share
36
+ RUN mkdir -p /usr/local/share/npm-global && \
37
+ chown -R node:node /usr/local/share
38
+
39
+ ARG USERNAME=node
40
+
41
+ # Persist bash history.
42
+ RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
43
+ && mkdir /commandhistory \
44
+ && touch /commandhistory/.bash_history \
45
+ && chown -R $USERNAME /commandhistory
46
+
47
+ # Set `DEVCONTAINER` environment variable to help with orientation
48
+ ENV DEVCONTAINER=true
49
+
50
+ # Create workspace and config directories and set permissions
51
+ RUN mkdir -p /workspace /home/node/.claude && \
52
+ chown -R node:node /workspace /home/node/.claude
53
+
54
+ WORKDIR /workspace
55
+
56
+ ARG GIT_DELTA_VERSION=0.18.2
57
+ RUN ARCH=$(dpkg --print-architecture) && \
58
+ wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
59
+ sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
60
+ rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
61
+
62
+ # Set up non-root user
63
+ USER node
64
+
65
+ # Install global packages
66
+ ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
67
+ ENV PATH=$PATH:/usr/local/share/npm-global/bin
68
+
69
+ # Set the default shell to zsh rather than sh
70
+ ENV SHELL=/bin/zsh
71
+
72
+ # Set the default editor and visual
73
+ ENV EDITOR=nano
74
+ ENV VISUAL=nano
75
+
76
+ # Default powerline10k theme
77
+ ARG ZSH_IN_DOCKER_VERSION=1.2.0
78
+ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
79
+ -p git \
80
+ -p fzf \
81
+ -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
82
+ -a "source /usr/share/doc/fzf/examples/completion.zsh" \
83
+ -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
84
+ -x
85
+
86
+ # Install Claude
87
+ RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
88
+
89
+ # Copy and set up firewall script
90
+ COPY init-firewall.sh /usr/local/bin/
91
+ USER root
92
+ RUN chmod +x /usr/local/bin/init-firewall.sh && \
93
+ echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
94
+ chmod 0440 /etc/sudoers.d/node-firewall
95
+ USER node
96
+ ```
97
+
98
+ ## What this Dockerfile provides
99
+
100
+ | Layer | Details |
101
+ |-------|---------|
102
+ | **Base image** | Node.js 20 (LTS) |
103
+ | **Dev tools** | git, ZSH, fzf, gh CLI, jq, nano, vim, delta (git diff) |
104
+ | **Network tools** | iptables, ipset, iproute2, dnsutils, aggregate (for firewall) |
105
+ | **User** | Non-root `node` user with sudo for firewall script only |
106
+ | **Shell** | ZSH with Powerlevel10k theme, fzf key bindings |
107
+ | **Claude Code** | Installed globally via npm (version configurable via build arg) |
108
+ | **Firewall** | `init-firewall.sh` copied and configured with passwordless sudo |
@@ -0,0 +1,292 @@
1
+ # init-firewall.sh — Reference
2
+
3
+ Write this file to `.devcontainer/init-firewall.sh`:
4
+
5
+ ```bash
6
+ #!/bin/bash
7
+ set -uo pipefail # Strict vars and pipelines, but handle errors manually
8
+ IFS=$'\n\t'
9
+
10
+ LOG="/tmp/firewall-init.log"
11
+ exec > >(tee -a "$LOG") 2>&1
12
+ echo "=== Firewall init started at $(date -u) ==="
13
+
14
+ # --- Diagnostics -----------------------------------------------------------
15
+ echo "--- Pre-flight checks ---"
16
+ HAVE_IPTABLES=false
17
+ HAVE_IPSET=false
18
+
19
+ if iptables -L -n >/dev/null 2>&1; then
20
+ HAVE_IPTABLES=true
21
+ echo "[OK] iptables is functional"
22
+ else
23
+ echo "[FAIL] iptables is NOT functional — firewall cannot be configured"
24
+ echo "Hint: ensure --cap-add=NET_ADMIN is set in devcontainer.json runArgs"
25
+ exit 1
26
+ fi
27
+
28
+ if ipset list >/dev/null 2>&1 || ipset create _test hash:net 2>/dev/null; then
29
+ ipset destroy _test 2>/dev/null || true
30
+ HAVE_IPSET=true
31
+ echo "[OK] ipset is functional"
32
+ else
33
+ echo "[WARN] ipset is NOT functional — falling back to iptables-only rules"
34
+ fi
35
+
36
+ if command -v dig >/dev/null 2>&1; then
37
+ echo "[OK] dig is available"
38
+ else
39
+ echo "[WARN] dig not found — DNS resolution will use getent"
40
+ fi
41
+
42
+ if command -v aggregate >/dev/null 2>&1; then
43
+ echo "[OK] aggregate is available"
44
+ else
45
+ echo "[WARN] aggregate not found — GitHub CIDRs will be added individually"
46
+ fi
47
+
48
+ # --- Helper: resolve domain to IPs ----------------------------------------
49
+ resolve_domain() {
50
+ local domain="$1"
51
+ local ips=""
52
+ if command -v dig >/dev/null 2>&1; then
53
+ ips=$(dig +noall +answer +short A "$domain" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
54
+ fi
55
+ if [ -z "$ips" ] && command -v getent >/dev/null 2>&1; then
56
+ ips=$(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
57
+ fi
58
+ echo "$ips"
59
+ }
60
+
61
+ # --- 1. Preserve Docker DNS -----------------------------------------------
62
+ echo "--- Preserving Docker DNS rules ---"
63
+ DOCKER_DNS_RULES=$(iptables-save -t nat 2>/dev/null | grep "127\.0\.0\.11" || true)
64
+
65
+ # --- 2. Flush existing rules -----------------------------------------------
66
+ echo "--- Flushing existing rules ---"
67
+ iptables -F || true
68
+ iptables -X || true
69
+ iptables -t nat -F || true
70
+ iptables -t nat -X || true
71
+ iptables -t mangle -F || true
72
+ iptables -t mangle -X || true
73
+ if [ "$HAVE_IPSET" = true ]; then
74
+ ipset destroy allowed-domains 2>/dev/null || true
75
+ fi
76
+
77
+ # --- 3. Restore Docker DNS -------------------------------------------------
78
+ if [ -n "$DOCKER_DNS_RULES" ]; then
79
+ echo "Restoring Docker DNS rules..."
80
+ iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
81
+ iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
82
+ echo "$DOCKER_DNS_RULES" | while read -r rule; do
83
+ iptables -t nat $rule 2>/dev/null || true
84
+ done
85
+ else
86
+ echo "No Docker DNS rules to restore"
87
+ fi
88
+
89
+ # --- 4. Base rules (DNS, SSH, localhost) ------------------------------------
90
+ echo "--- Setting base rules ---"
91
+ # Outbound DNS
92
+ iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
93
+ iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
94
+ # Inbound DNS responses
95
+ iptables -A INPUT -p udp --sport 53 -j ACCEPT
96
+ iptables -A INPUT -p tcp --sport 53 -j ACCEPT
97
+ # Outbound SSH
98
+ iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
99
+ iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
100
+ # Localhost
101
+ iptables -A INPUT -i lo -j ACCEPT
102
+ iptables -A OUTPUT -o lo -j ACCEPT
103
+
104
+ # --- 5. Host network -------------------------------------------------------
105
+ HOST_IP=$(ip route 2>/dev/null | grep default | head -1 | awk '{print $3}')
106
+ if [ -n "$HOST_IP" ]; then
107
+ HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/16/")
108
+ echo "Host network: $HOST_NETWORK (via $HOST_IP)"
109
+ iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
110
+ iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
111
+ else
112
+ echo "[WARN] Could not detect host IP — allowing RFC1918 ranges for Docker connectivity"
113
+ iptables -A INPUT -s 172.16.0.0/12 -j ACCEPT
114
+ iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
115
+ iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT
116
+ iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
117
+ iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT
118
+ iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
119
+ fi
120
+
121
+ # --- 6. Allowed domains ----------------------------------------------------
122
+ ALLOWED_DOMAINS=(
123
+ "registry.npmjs.org"
124
+ "api.anthropic.com"
125
+ "sentry.io"
126
+ "statsig.anthropic.com"
127
+ "statsig.com"
128
+ "marketplace.visualstudio.com"
129
+ "vscode.blob.core.windows.net"
130
+ "update.code.visualstudio.com"
131
+ )
132
+
133
+ if [ "$HAVE_IPSET" = true ]; then
134
+ # ---- ipset mode (preferred) ----
135
+ echo "--- Building ipset allowlist ---"
136
+ ipset create allowed-domains hash:net
137
+
138
+ # GitHub dynamic IPs
139
+ echo "Fetching GitHub IP ranges..."
140
+ gh_ranges=$(curl -sf --connect-timeout 10 https://api.github.com/meta 2>/dev/null || true)
141
+ if [ -n "$gh_ranges" ] && echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null 2>&1; then
142
+ if command -v aggregate >/dev/null 2>&1; then
143
+ while read -r cidr; do
144
+ [[ "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] && ipset add allowed-domains "$cidr" 2>/dev/null || true
145
+ done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q 2>/dev/null)
146
+ else
147
+ while read -r cidr; do
148
+ [[ "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] && ipset add allowed-domains "$cidr" 2>/dev/null || true
149
+ done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]')
150
+ fi
151
+ echo "[OK] GitHub IP ranges added"
152
+ else
153
+ echo "[WARN] Could not fetch GitHub IPs — resolving github.com directly"
154
+ for gh_domain in "github.com" "api.github.com" "raw.githubusercontent.com" "objects.githubusercontent.com"; do
155
+ ips=$(resolve_domain "$gh_domain")
156
+ while read -r ip; do
157
+ [ -n "$ip" ] && ipset add allowed-domains "$ip" 2>/dev/null || true
158
+ done <<< "$ips"
159
+ done
160
+ fi
161
+
162
+ # Other allowed domains
163
+ for domain in "${ALLOWED_DOMAINS[@]}"; do
164
+ echo "Resolving $domain..."
165
+ ips=$(resolve_domain "$domain")
166
+ if [ -z "$ips" ]; then
167
+ echo "[WARN] Failed to resolve $domain — skipping"
168
+ continue
169
+ fi
170
+ while read -r ip; do
171
+ [ -n "$ip" ] && ipset add allowed-domains "$ip" 2>/dev/null || true
172
+ done <<< "$ips"
173
+ done
174
+
175
+ # Apply ipset match rule
176
+ iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
177
+
178
+ else
179
+ # ---- iptables-only mode (fallback) ----
180
+ echo "--- Building iptables allowlist (no ipset) ---"
181
+
182
+ # GitHub IPs
183
+ echo "Fetching GitHub IP ranges..."
184
+ gh_ranges=$(curl -sf --connect-timeout 10 https://api.github.com/meta 2>/dev/null || true)
185
+ if [ -n "$gh_ranges" ] && echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null 2>&1; then
186
+ while read -r cidr; do
187
+ [[ "$cidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] && \
188
+ iptables -A OUTPUT -d "$cidr" -j ACCEPT 2>/dev/null || true
189
+ done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]')
190
+ echo "[OK] GitHub IP ranges added via iptables"
191
+ else
192
+ echo "[WARN] Could not fetch GitHub IPs — resolving github.com directly"
193
+ for gh_domain in "github.com" "api.github.com" "raw.githubusercontent.com" "objects.githubusercontent.com"; do
194
+ ips=$(resolve_domain "$gh_domain")
195
+ while read -r ip; do
196
+ [ -n "$ip" ] && iptables -A OUTPUT -d "$ip" -j ACCEPT 2>/dev/null || true
197
+ done <<< "$ips"
198
+ done
199
+ fi
200
+
201
+ # Other allowed domains
202
+ for domain in "${ALLOWED_DOMAINS[@]}"; do
203
+ echo "Resolving $domain..."
204
+ ips=$(resolve_domain "$domain")
205
+ if [ -z "$ips" ]; then
206
+ echo "[WARN] Failed to resolve $domain — skipping"
207
+ continue
208
+ fi
209
+ while read -r ip; do
210
+ [ -n "$ip" ] && iptables -A OUTPUT -d "$ip" -j ACCEPT 2>/dev/null || true
211
+ done <<< "$ips"
212
+ done
213
+ fi
214
+
215
+ # --- 7. Default-deny policies ----------------------------------------------
216
+ echo "--- Applying default-deny policies ---"
217
+ iptables -P INPUT DROP
218
+ iptables -P FORWARD DROP
219
+ iptables -P OUTPUT DROP
220
+
221
+ # Allow established connections
222
+ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
223
+ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
224
+
225
+ # Reject everything else with immediate feedback
226
+ iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
227
+
228
+ # --- 8. Verification -------------------------------------------------------
229
+ echo "--- Verifying firewall ---"
230
+ VERIFIED=true
231
+
232
+ if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
233
+ echo "[FAIL] Firewall verification failed — reached https://example.com (should be blocked)"
234
+ VERIFIED=false
235
+ else
236
+ echo "[OK] https://example.com is blocked as expected"
237
+ fi
238
+
239
+ if curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
240
+ echo "[OK] https://api.github.com is reachable as expected"
241
+ else
242
+ echo "[WARN] https://api.github.com is not reachable — GitHub IPs may have changed"
243
+ echo " Claude Code will still work, but git operations may fail"
244
+ fi
245
+
246
+ if [ "$VERIFIED" = false ]; then
247
+ echo "=== FIREWALL VERIFICATION FAILED ==="
248
+ exit 1
249
+ fi
250
+
251
+ echo "=== Firewall configured successfully ==="
252
+ echo "Mode: $([ "$HAVE_IPSET" = true ] && echo 'ipset' || echo 'iptables-only')"
253
+ echo "Log: $LOG"
254
+ ```
255
+
256
+ ## Key improvements over the original
257
+
258
+ | Issue | Original behavior | Improved behavior |
259
+ |-------|-------------------|-------------------|
260
+ | `ipset` not available | Fatal crash | Falls back to iptables-only rules |
261
+ | `aggregate` not available | Fatal crash | Adds CIDRs individually without aggregation |
262
+ | `dig` not available | Fatal crash | Falls back to `getent ahostsv4` |
263
+ | GitHub API unreachable | Fatal crash | Resolves github.com/api.github.com directly |
264
+ | Domain resolution fails | Fatal crash | Skips domain with warning, continues |
265
+ | Host IP detection fails | Fatal crash | Allows all RFC1918 ranges as fallback |
266
+ | Docker DNS restore fails | Silent + potential crash | Handles per-rule with `|| true` |
267
+ | No diagnostic output | Blind exit code 1 | Full log at `/tmp/firewall-init.log` |
268
+ | GitHub unreachable after setup | Fatal crash | Warning only (Claude API is the critical path) |
269
+
270
+ ## Firewall rules summary
271
+
272
+ | Rule | Direction | Purpose |
273
+ |------|-----------|---------|
274
+ | DNS (UDP/TCP 53) | Outbound | Domain name resolution |
275
+ | SSH (TCP 22) | Outbound | Git over SSH |
276
+ | Localhost | Both | Container-internal communication |
277
+ | Host network | Both | Docker host ↔ container communication |
278
+ | GitHub IPs | Outbound | Git operations, GitHub API (dynamically fetched or resolved) |
279
+ | npm registry | Outbound | Package installation |
280
+ | api.anthropic.com | Outbound | Claude API calls |
281
+ | sentry.io | Outbound | Error reporting |
282
+ | statsig.anthropic.com | Outbound | Feature flags |
283
+ | VS Code marketplace | Outbound | Extension downloads |
284
+ | **Everything else** | **BLOCKED** | Default-deny with REJECT |
285
+
286
+ ## Verification on startup
287
+
288
+ The script verifies the firewall by:
289
+ 1. Confirming `https://example.com` is **blocked** (must fail — otherwise exit 1)
290
+ 2. Confirming `https://api.github.com` is **reachable** (warning only if it fails)
291
+
292
+ All output is logged to `/tmp/firewall-init.log` for debugging.