@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +13 -0
- package/README.md +4 -2
- package/package.json +2 -2
- package/skills/debug/SKILL.md +4 -4
- package/skills/yolo/SKILL.md +331 -0
- package/skills/yolo/references/devcontainer-json.md +74 -0
- package/skills/yolo/references/dockerfile.md +108 -0
- package/skills/yolo/references/init-firewall.sh.md +292 -0
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
"url": "https://github.com/jgamaraalv"
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
|
-
"description": "15 agents and
|
|
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
|
|
15
|
-
"version": "3.
|
|
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.
|
|
4
|
-
"description": "15 specialized agents and
|
|
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 +
|
|
3
|
+
> 15 specialized agents + 22 curated skills for TypeScript fullstack development
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@jgamaraalv/ts-dev-kit)
|
|
6
6
|
[](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 (
|
|
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.
|
|
4
|
-
"description": "Claude Code plugin: 15 agents +
|
|
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": {
|
package/skills/debug/SKILL.md
CHANGED
|
@@ -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.
|