cc-sandboxer 1.0.0
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/LICENSE +21 -0
- package/README.md +515 -0
- package/README.vi.md +509 -0
- package/README.zh.md +508 -0
- package/cc-sandboxer.sh +682 -0
- package/docker/Dockerfile +65 -0
- package/docker/init-firewall.sh +126 -0
- package/package.json +46 -0
- package/vscode/tasks.json +61 -0
package/cc-sandboxer.sh
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔══════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ 🧠 Claude Code Sandbox ║
|
|
4
|
+
# ║ Run claude --dangerously-skip-permissions safely in Docker ║
|
|
5
|
+
# ║ Works with Docker Desktop, OrbStack, Colima & VS Code ║
|
|
6
|
+
# ╚══════════════════════════════════════════════════════════════════╝
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# ── Version ──────────────────────────────────────────────────
|
|
11
|
+
SCRIPT_VERSION="1.0.0"
|
|
12
|
+
|
|
13
|
+
# ── Config ───────────────────────────────────────────────────
|
|
14
|
+
IMAGE_NAME="cc-sandboxer"
|
|
15
|
+
IMAGE_TAG="latest"
|
|
16
|
+
CONTAINER_NAME="cc-sandboxer"
|
|
17
|
+
TZ="${TZ:-Asia/Ho_Chi_Minh}"
|
|
18
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
19
|
+
while [[ -L "$SOURCE" ]]; do
|
|
20
|
+
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
21
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
22
|
+
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
|
|
23
|
+
done
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
25
|
+
|
|
26
|
+
# ── Colors & Styles ──────────────────────────────────────────
|
|
27
|
+
RED='\033[0;31m'
|
|
28
|
+
GREEN='\033[0;32m'
|
|
29
|
+
YELLOW='\033[1;33m'
|
|
30
|
+
MAGENTA='\033[0;35m'
|
|
31
|
+
CYAN='\033[0;36m'
|
|
32
|
+
WHITE='\033[1;37m'
|
|
33
|
+
GRAY='\033[0;90m'
|
|
34
|
+
BOLD='\033[1m'
|
|
35
|
+
DIM='\033[2m'
|
|
36
|
+
UNDERLINE='\033[4m'
|
|
37
|
+
NC='\033[0m'
|
|
38
|
+
|
|
39
|
+
# ── Emoji icons ──────────────────────────────────────────────
|
|
40
|
+
I_ROCKET="🚀"
|
|
41
|
+
I_BRAIN="🧠"
|
|
42
|
+
I_SHIELD="🛡️"
|
|
43
|
+
I_LOCK="🔒"
|
|
44
|
+
I_CHECK="✅"
|
|
45
|
+
I_CROSS="❌"
|
|
46
|
+
I_WARN="⚠️"
|
|
47
|
+
I_INFO="💡"
|
|
48
|
+
I_FOLDER="📁"
|
|
49
|
+
I_DOCKER="🐳"
|
|
50
|
+
I_GEAR="⚙️"
|
|
51
|
+
I_KEY="🔑"
|
|
52
|
+
I_GLOBE="🌐"
|
|
53
|
+
I_CLOCK="⏱️"
|
|
54
|
+
I_PACKAGE="📦"
|
|
55
|
+
I_SHELL="🐚"
|
|
56
|
+
I_SPARKLE="✨"
|
|
57
|
+
I_LINK="🔗"
|
|
58
|
+
I_ZAP="⚡"
|
|
59
|
+
I_PLUG="🔌"
|
|
60
|
+
I_VSCODE="💻"
|
|
61
|
+
|
|
62
|
+
# ── Logging ──────────────────────────────────────────────────
|
|
63
|
+
log() { echo -e " ${GREEN}${I_CHECK}${NC} $*"; }
|
|
64
|
+
err() { echo -e " ${RED}${I_CROSS}${NC} ${RED}$*${NC}" >&2; }
|
|
65
|
+
step() { echo -e " ${MAGENTA}${I_GEAR}${NC} ${BOLD}$*${NC}"; }
|
|
66
|
+
success() { echo -e " ${GREEN}${I_SPARKLE}${NC} ${GREEN}${BOLD}$*${NC}"; }
|
|
67
|
+
|
|
68
|
+
divider() {
|
|
69
|
+
echo -e " ${DIM}─────────────────────────────────────────────────────────${NC}"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# ── Banner ───────────────────────────────────────────────────
|
|
73
|
+
show_banner() {
|
|
74
|
+
echo ""
|
|
75
|
+
echo -e "${BOLD}${CYAN}"
|
|
76
|
+
cat << 'BANNER'
|
|
77
|
+
██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
|
|
78
|
+
██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
|
|
79
|
+
██║ ██║ ███████║██║ ██║██║ ██║█████╗
|
|
80
|
+
██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
|
|
81
|
+
╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
|
|
82
|
+
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
83
|
+
BANNER
|
|
84
|
+
echo -e "${NC}"
|
|
85
|
+
echo -e " ${BOLD}${WHITE}C O D E S A N D B O X${NC} ${DIM}v${SCRIPT_VERSION}${NC}"
|
|
86
|
+
echo -e " ${DIM}Run skip-permissions safely in a sandbox${NC}"
|
|
87
|
+
echo ""
|
|
88
|
+
divider
|
|
89
|
+
echo ""
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# ── Help ─────────────────────────────────────────────────────
|
|
93
|
+
show_help() {
|
|
94
|
+
show_banner
|
|
95
|
+
echo -e " ${BOLD}${WHITE}USAGE${NC}"
|
|
96
|
+
echo ""
|
|
97
|
+
echo -e " ${GREEN}cc-sandboxer${NC} ${DIM}[project_path]${NC} ${DIM}[options]${NC}"
|
|
98
|
+
echo ""
|
|
99
|
+
echo -e " ${BOLD}${WHITE}MODES${NC}"
|
|
100
|
+
echo ""
|
|
101
|
+
echo -e " ${I_ROCKET} ${BOLD}CLI Mode${NC} ${DIM}(default)${NC} — Run directly from terminal"
|
|
102
|
+
echo -e " ${I_VSCODE} ${BOLD}VS Code Mode${NC} — Use ${GREEN}--init${NC} to setup DevContainer"
|
|
103
|
+
echo ""
|
|
104
|
+
echo -e " ${BOLD}${WHITE}ARGUMENTS${NC}"
|
|
105
|
+
echo ""
|
|
106
|
+
echo -e " ${WHITE}project_path${NC} Path to project ${DIM}(default: current directory)${NC}"
|
|
107
|
+
echo ""
|
|
108
|
+
echo -e " ${BOLD}${WHITE}OPTIONS — Setup${NC}"
|
|
109
|
+
echo ""
|
|
110
|
+
echo -e " ${GREEN}--init${NC} ${I_VSCODE} Setup devcontainer + VS Code tasks in project"
|
|
111
|
+
echo -e " ${GREEN}--rebuild${NC} ${I_PACKAGE} Force rebuild Docker image"
|
|
112
|
+
echo -e " ${GREEN}--version${NC}, ${GREEN}-v${NC} ${I_INFO} Show version"
|
|
113
|
+
echo -e " ${GREEN}--help${NC}, ${GREEN}-h${NC} ${I_INFO} Show this help"
|
|
114
|
+
echo ""
|
|
115
|
+
echo -e " ${BOLD}${WHITE}OPTIONS — Runtime${NC}"
|
|
116
|
+
echo ""
|
|
117
|
+
echo -e " ${GREEN}--shell${NC} ${I_SHELL} Open shell only (don't start Claude)"
|
|
118
|
+
echo -e " ${GREEN}--no-firewall${NC} ${I_GLOBE} Skip firewall setup"
|
|
119
|
+
echo -e " ${GREEN}--allow-domain${NC} ${DIM}NAME${NC} ${I_PLUG} Whitelist extra domain ${DIM}(repeatable)${NC}"
|
|
120
|
+
echo -e " ${GREEN}--continue${NC}, ${GREEN}-c${NC} ${I_LINK} Resume previous conversation"
|
|
121
|
+
echo -e " ${GREEN}-p${NC} ${DIM}\"prompt\"${NC} ${I_ZAP} One-shot task mode"
|
|
122
|
+
echo -e " ${GREEN}--disallowedTools${NC} ${DIM}T${NC} ${I_SHIELD} Block specific tools"
|
|
123
|
+
echo ""
|
|
124
|
+
echo -e " ${BOLD}${WHITE}EXAMPLES${NC}"
|
|
125
|
+
echo ""
|
|
126
|
+
echo -e " ${DIM}# Quick start — interactive mode${NC}"
|
|
127
|
+
echo -e " ${GREEN}\$${NC} cc-sandboxer"
|
|
128
|
+
echo ""
|
|
129
|
+
echo -e " ${DIM}# Setup VS Code DevContainer in your project${NC}"
|
|
130
|
+
echo -e " ${GREEN}\$${NC} cc-sandboxer --init ~/projects/my-app"
|
|
131
|
+
echo ""
|
|
132
|
+
echo -e " ${DIM}# One-shot task${NC}"
|
|
133
|
+
echo -e " ${GREEN}\$${NC} cc-sandboxer . -p \"Refactor auth and write tests\""
|
|
134
|
+
echo ""
|
|
135
|
+
echo -e " ${DIM}# Resume last conversation${NC}"
|
|
136
|
+
echo -e " ${GREEN}\$${NC} cc-sandboxer . --continue"
|
|
137
|
+
echo ""
|
|
138
|
+
echo -e " ${DIM}# Safe mode — block rm commands${NC}"
|
|
139
|
+
echo -e " ${GREEN}\$${NC} cc-sandboxer . --disallowedTools \"Bash(rm:*)\""
|
|
140
|
+
echo ""
|
|
141
|
+
echo -e " ${BOLD}${WHITE}ENVIRONMENT${NC}"
|
|
142
|
+
echo ""
|
|
143
|
+
echo -e " ${WHITE}TZ${NC} Timezone ${DIM}(default: Asia/Ho_Chi_Minh)${NC}"
|
|
144
|
+
echo ""
|
|
145
|
+
divider
|
|
146
|
+
echo ""
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# ══════════════════════════════════════════════════════════════
|
|
150
|
+
# 💻 VS Code Integration — --init command
|
|
151
|
+
# ══════════════════════════════════════════════════════════════
|
|
152
|
+
|
|
153
|
+
init_vscode_project() {
|
|
154
|
+
local target_path="$1"
|
|
155
|
+
|
|
156
|
+
show_banner
|
|
157
|
+
step "Setting up VS Code DevContainer ${I_VSCODE}"
|
|
158
|
+
echo ""
|
|
159
|
+
echo -e " ${DIM}Target:${NC} ${WHITE}${target_path}${NC}"
|
|
160
|
+
echo ""
|
|
161
|
+
|
|
162
|
+
# ── Create devcontainer/ ────────────────────────────────
|
|
163
|
+
local dc_dir="${target_path}/devcontainer"
|
|
164
|
+
mkdir -p "$dc_dir"
|
|
165
|
+
|
|
166
|
+
# Dockerfile — copy from the project's single source of truth
|
|
167
|
+
cp "${SCRIPT_DIR}/docker/Dockerfile" "${dc_dir}/Dockerfile"
|
|
168
|
+
show_progress_line "Created devcontainer/Dockerfile" "done"
|
|
169
|
+
|
|
170
|
+
# Firewall script — copy from the project's init-firewall.sh
|
|
171
|
+
cp "${SCRIPT_DIR}/docker/init-firewall.sh" "${dc_dir}/init-firewall.sh"
|
|
172
|
+
chmod +x "${dc_dir}/init-firewall.sh"
|
|
173
|
+
show_progress_line "Created devcontainer/init-firewall.sh" "done"
|
|
174
|
+
|
|
175
|
+
# devcontainer.json — copy from the project's single source of truth
|
|
176
|
+
cp "${SCRIPT_DIR}/devcontainer/devcontainer.json" "${dc_dir}/devcontainer.json"
|
|
177
|
+
# Fix Dockerfile path: in target project, Dockerfile is local to devcontainer/
|
|
178
|
+
sed -i.bak 's|"dockerfile": "\.\./docker/Dockerfile"|"dockerfile": "Dockerfile"|' "${dc_dir}/devcontainer.json"
|
|
179
|
+
sed -i.bak '/"context": "\.\.",/d' "${dc_dir}/devcontainer.json"
|
|
180
|
+
rm -f "${dc_dir}/devcontainer.json.bak"
|
|
181
|
+
show_progress_line "Created devcontainer/devcontainer.json" "done"
|
|
182
|
+
|
|
183
|
+
# ── Create .vscode/tasks.json ────────────────────────────
|
|
184
|
+
local vscode_dir="${target_path}/.vscode"
|
|
185
|
+
mkdir -p "$vscode_dir"
|
|
186
|
+
|
|
187
|
+
# Only write tasks.json if it doesn't exist (don't clobber user's tasks)
|
|
188
|
+
if [[ ! -f "${vscode_dir}/tasks.json" ]]; then
|
|
189
|
+
cp "${SCRIPT_DIR}/vscode/tasks.json" "${vscode_dir}/tasks.json"
|
|
190
|
+
show_progress_line "Created .vscode/tasks.json" "done"
|
|
191
|
+
else
|
|
192
|
+
show_progress_line "Skipped .vscode/tasks.json (already exists)" "skip"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── Summary ──────────────────────────────────────────────
|
|
197
|
+
echo ""
|
|
198
|
+
divider
|
|
199
|
+
echo ""
|
|
200
|
+
success "VS Code DevContainer setup complete! ${I_SPARKLE}"
|
|
201
|
+
echo ""
|
|
202
|
+
echo -e " ${BOLD}${WHITE}Files created:${NC}"
|
|
203
|
+
echo ""
|
|
204
|
+
echo -e " ${DIM}${target_path}/${NC}"
|
|
205
|
+
echo -e " ${DIM}├── ${NC}${WHITE}devcontainer/${NC}"
|
|
206
|
+
echo -e " ${DIM}│ ├── ${NC}${CYAN}Dockerfile${NC}"
|
|
207
|
+
echo -e " ${DIM}│ ├── ${NC}${CYAN}devcontainer.json${NC}"
|
|
208
|
+
echo -e " ${DIM}│ └── ${NC}${CYAN}init-firewall.sh${NC}"
|
|
209
|
+
echo -e " ${DIM}└── ${NC}${WHITE}.vscode/${NC}"
|
|
210
|
+
echo -e " ${DIM} └── ${NC}${CYAN}tasks.json${NC}"
|
|
211
|
+
echo ""
|
|
212
|
+
divider
|
|
213
|
+
echo ""
|
|
214
|
+
echo -e " ${BOLD}${WHITE}Next steps:${NC}"
|
|
215
|
+
echo ""
|
|
216
|
+
echo -e " ${YELLOW}1.${NC} Open the project in VS Code"
|
|
217
|
+
echo -e " ${GREEN}\$${NC} code ${target_path}"
|
|
218
|
+
echo ""
|
|
219
|
+
echo -e " ${YELLOW}2.${NC} Reopen in Container"
|
|
220
|
+
echo -e " ${DIM}Cmd+Shift+P → \"Dev Containers: Reopen in Container\"${NC}"
|
|
221
|
+
echo ""
|
|
222
|
+
echo -e " ${YELLOW}3.${NC} Login (first time only)"
|
|
223
|
+
echo -e " ${DIM}Cmd+Shift+P → \"Tasks: Run Task\" → \"${I_KEY} Claude: Login\"${NC}"
|
|
224
|
+
echo ""
|
|
225
|
+
echo -e " ${YELLOW}4.${NC} Start Claude!"
|
|
226
|
+
echo -e " ${DIM}Cmd+Shift+P → \"Tasks: Run Task\" → \"${I_BRAIN} Claude: Skip Permissions\"${NC}"
|
|
227
|
+
echo ""
|
|
228
|
+
echo -e " ${BOLD}${WHITE}Available VS Code Tasks:${NC}"
|
|
229
|
+
echo ""
|
|
230
|
+
echo -e " ${I_BRAIN} ${WHITE}Claude: Skip Permissions${NC} ${DIM}— Interactive mode${NC}"
|
|
231
|
+
echo -e " ${I_LINK} ${WHITE}Claude: Resume Last Chat${NC} ${DIM}— Continue previous chat${NC}"
|
|
232
|
+
echo -e " ${I_ZAP} ${WHITE}Claude: One-Shot Task${NC} ${DIM}— Custom prompt input${NC}"
|
|
233
|
+
echo -e " ${I_SHIELD} ${WHITE}Claude: Safe Mode (no rm)${NC} ${DIM}— Block file deletion${NC}"
|
|
234
|
+
echo -e " ${I_LOCK} ${WHITE}Firewall: Re-initialize${NC} ${DIM}— Re-apply firewall${NC}"
|
|
235
|
+
echo -e " ${I_KEY} ${WHITE}Claude: Login${NC} ${DIM}— First-time auth${NC}"
|
|
236
|
+
echo ""
|
|
237
|
+
divider
|
|
238
|
+
echo ""
|
|
239
|
+
echo -e " ${YELLOW}${I_WARN} ${BOLD}Claude Code Extension:${NC}"
|
|
240
|
+
echo -e " ${DIM}If the extension is disabled inside the container, install it manually:${NC}"
|
|
241
|
+
echo -e " ${WHITE}Cmd+Shift+X${NC} ${DIM}→${NC} ${WHITE}Search \"Claude Code\"${NC} ${DIM}→${NC} ${WHITE}Install in Dev Container${NC}"
|
|
242
|
+
echo -e " ${DIM}This only needs to be done once — it persists across rebuilds.${NC}"
|
|
243
|
+
echo ""
|
|
244
|
+
divider
|
|
245
|
+
echo ""
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# ── Progress line helper ─────────────────────────────────────
|
|
249
|
+
show_progress_line() {
|
|
250
|
+
local label="$1"
|
|
251
|
+
local status="$2"
|
|
252
|
+
|
|
253
|
+
case "$status" in
|
|
254
|
+
running) echo -e " ${CYAN}▸${NC} ${label} ${DIM}...${NC}" ;;
|
|
255
|
+
done) echo -e " ${GREEN}●${NC} ${label}" ;;
|
|
256
|
+
skip) echo -e " ${YELLOW}○${NC} ${label}" ;;
|
|
257
|
+
fail) echo -e " ${RED}✖${NC} ${label}" ;;
|
|
258
|
+
esac
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ══════════════════════════════════════════════════════════════
|
|
262
|
+
# 🐳 CLI Mode — Docker direct
|
|
263
|
+
# ══════════════════════════════════════════════════════════════
|
|
264
|
+
|
|
265
|
+
# ── Parse args ───────────────────────────────────────────────
|
|
266
|
+
PROJECT_PATH=""
|
|
267
|
+
CLAUDE_ARGS=()
|
|
268
|
+
FORCE_REBUILD=false
|
|
269
|
+
SHELL_ONLY=false
|
|
270
|
+
NO_FIREWALL=false
|
|
271
|
+
INIT_MODE=false
|
|
272
|
+
EXTRA_DOMAINS=()
|
|
273
|
+
|
|
274
|
+
while [[ $# -gt 0 ]]; do
|
|
275
|
+
case "$1" in
|
|
276
|
+
--init)
|
|
277
|
+
INIT_MODE=true; shift ;;
|
|
278
|
+
--rebuild)
|
|
279
|
+
FORCE_REBUILD=true; shift ;;
|
|
280
|
+
--shell)
|
|
281
|
+
SHELL_ONLY=true; shift ;;
|
|
282
|
+
--no-firewall)
|
|
283
|
+
NO_FIREWALL=true; shift ;;
|
|
284
|
+
--allow-domain)
|
|
285
|
+
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
|
286
|
+
err "--allow-domain requires a domain argument"
|
|
287
|
+
exit 1
|
|
288
|
+
fi
|
|
289
|
+
# Validate domain format (alphanumeric, dots, hyphens, wildcards)
|
|
290
|
+
if [[ ! "$2" =~ ^[a-zA-Z0-9.*-]+$ ]]; then
|
|
291
|
+
err "Invalid domain format: $2"
|
|
292
|
+
exit 1
|
|
293
|
+
fi
|
|
294
|
+
EXTRA_DOMAINS+=("$2"); shift 2 ;;
|
|
295
|
+
--continue|-c)
|
|
296
|
+
CLAUDE_ARGS+=("--continue"); shift ;;
|
|
297
|
+
-p)
|
|
298
|
+
CLAUDE_ARGS+=("-p" "$2"); shift 2 ;;
|
|
299
|
+
--disallowedTools)
|
|
300
|
+
CLAUDE_ARGS+=("--disallowedTools" "$2"); shift 2 ;;
|
|
301
|
+
--version|-v)
|
|
302
|
+
echo "cc-sandboxer v${SCRIPT_VERSION}"; exit 0 ;;
|
|
303
|
+
--help|-h)
|
|
304
|
+
show_help; exit 0 ;;
|
|
305
|
+
-*)
|
|
306
|
+
CLAUDE_ARGS+=("$1"); shift ;;
|
|
307
|
+
*)
|
|
308
|
+
if [[ -z "$PROJECT_PATH" ]]; then
|
|
309
|
+
PROJECT_PATH="$1"
|
|
310
|
+
else
|
|
311
|
+
CLAUDE_ARGS+=("$1")
|
|
312
|
+
fi
|
|
313
|
+
shift ;;
|
|
314
|
+
esac
|
|
315
|
+
done
|
|
316
|
+
|
|
317
|
+
# Default project path
|
|
318
|
+
if [[ -z "$PROJECT_PATH" ]]; then
|
|
319
|
+
PROJECT_PATH="$(pwd)"
|
|
320
|
+
fi
|
|
321
|
+
# Create directory if it doesn't exist (useful for --init with new projects)
|
|
322
|
+
if [[ ! -d "$PROJECT_PATH" ]]; then
|
|
323
|
+
if [[ "$INIT_MODE" == "true" ]]; then
|
|
324
|
+
mkdir -p "$PROJECT_PATH"
|
|
325
|
+
else
|
|
326
|
+
err "Project path not found: ${PROJECT_PATH:-<empty>}"
|
|
327
|
+
echo ""
|
|
328
|
+
echo -e " ${BOLD}${WHITE}Usage:${NC}"
|
|
329
|
+
echo -e " ${GREEN}cc-sandboxer${NC} ${DIM}[project_path]${NC}"
|
|
330
|
+
echo -e " ${GREEN}cc-sandboxer --init${NC} ${DIM}~/projects/my-app${NC}"
|
|
331
|
+
echo ""
|
|
332
|
+
exit 1
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
336
|
+
PROJECT_NAME="$(basename "$PROJECT_PATH")"
|
|
337
|
+
|
|
338
|
+
# ── Handle --init mode ──────────────────────────────────────
|
|
339
|
+
if [[ "$INIT_MODE" == "true" ]]; then
|
|
340
|
+
init_vscode_project "$PROJECT_PATH"
|
|
341
|
+
exit 0
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# ── Detect container runtime ─────────────────────────────────
|
|
345
|
+
detect_runtime() {
|
|
346
|
+
step "Detecting container runtime..."
|
|
347
|
+
|
|
348
|
+
if ! command -v docker &>/dev/null; then
|
|
349
|
+
err "Docker not found!"
|
|
350
|
+
echo ""
|
|
351
|
+
echo -e " ${BOLD}Install one of these:${NC}"
|
|
352
|
+
echo -e " ${I_DOCKER} Docker Desktop ${DIM}→${NC} ${UNDERLINE}https://docker.com/products/docker-desktop${NC}"
|
|
353
|
+
echo -e " ${I_ROCKET} OrbStack (Mac) ${DIM}→${NC} ${UNDERLINE}https://orbstack.dev${NC}"
|
|
354
|
+
echo ""
|
|
355
|
+
exit 1
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Check if Docker daemon is actually running
|
|
359
|
+
if ! docker info &>/dev/null; then
|
|
360
|
+
err "Docker daemon is not running!"
|
|
361
|
+
echo ""
|
|
362
|
+
echo -e " ${BOLD}Start your container runtime:${NC}"
|
|
363
|
+
echo -e " ${I_DOCKER} Docker Desktop ${DIM}→${NC} Open the Docker Desktop app"
|
|
364
|
+
echo -e " ${I_ROCKET} OrbStack ${DIM}→${NC} Open the OrbStack app"
|
|
365
|
+
echo -e " ${I_DOCKER} Colima ${DIM}→${NC} Run ${GREEN}colima start${NC}"
|
|
366
|
+
echo ""
|
|
367
|
+
exit 1
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
RUNTIME="docker"
|
|
371
|
+
local docker_info
|
|
372
|
+
docker_info=$(docker info 2>/dev/null || true)
|
|
373
|
+
|
|
374
|
+
# Check context name first (most reliable), then fall back to docker info
|
|
375
|
+
local docker_context
|
|
376
|
+
docker_context=$(docker context show 2>/dev/null || echo "")
|
|
377
|
+
|
|
378
|
+
if [[ "$docker_context" == "orbstack" ]]; then
|
|
379
|
+
RUNTIME_LABEL="OrbStack"
|
|
380
|
+
RUNTIME_ICON="${I_ROCKET}"
|
|
381
|
+
elif [[ "$docker_context" == "colima"* ]]; then
|
|
382
|
+
RUNTIME_LABEL="Colima"
|
|
383
|
+
RUNTIME_ICON="🦙"
|
|
384
|
+
elif [[ "$docker_context" == "desktop-linux" ]] || echo "$docker_info" | grep -qi "docker desktop"; then
|
|
385
|
+
RUNTIME_LABEL="Docker Desktop"
|
|
386
|
+
RUNTIME_ICON="${I_DOCKER}"
|
|
387
|
+
else
|
|
388
|
+
RUNTIME_LABEL="Docker Engine"
|
|
389
|
+
RUNTIME_ICON="${I_DOCKER}"
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
log "Found ${BOLD}${RUNTIME_LABEL}${NC} ${RUNTIME_ICON}"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# ── Build image ──────────────────────────────────────────────
|
|
396
|
+
build_image() {
|
|
397
|
+
if [[ "$FORCE_REBUILD" == "true" ]] || ! docker image inspect "$IMAGE_NAME:$IMAGE_TAG" &>/dev/null; then
|
|
398
|
+
echo ""
|
|
399
|
+
step "Building sandbox image ${I_PACKAGE}"
|
|
400
|
+
echo ""
|
|
401
|
+
echo -e " ${DIM}Image:${NC} ${WHITE}${IMAGE_NAME}:${IMAGE_TAG}${NC}"
|
|
402
|
+
echo -e " ${DIM}Timezone:${NC} ${WHITE}${TZ}${NC}"
|
|
403
|
+
echo ""
|
|
404
|
+
|
|
405
|
+
echo -e " ${DIM}${I_CLOCK} Building... (first run takes 2-3 min)${NC}"
|
|
406
|
+
echo ""
|
|
407
|
+
|
|
408
|
+
local build_log
|
|
409
|
+
build_log=$(mktemp /tmp/cc-sandboxer-build-XXXXXXXX)
|
|
410
|
+
|
|
411
|
+
# Count total steps from Dockerfile (each instruction = 1 step)
|
|
412
|
+
local total_steps
|
|
413
|
+
total_steps=$(grep -cE '^\s*(FROM|RUN|COPY|ADD|ENV|ARG|WORKDIR|USER|ENTRYPOINT|CMD|EXPOSE|VOLUME|LABEL|SHELL)\b' "${SCRIPT_DIR}/docker/Dockerfile")
|
|
414
|
+
local current_step=0
|
|
415
|
+
local bar_width=40
|
|
416
|
+
|
|
417
|
+
docker build \
|
|
418
|
+
--build-arg "TZ=$TZ" \
|
|
419
|
+
-t "$IMAGE_NAME:$IMAGE_TAG" \
|
|
420
|
+
"${SCRIPT_DIR}/docker" > "$build_log" 2>&1 &
|
|
421
|
+
local build_pid=$!
|
|
422
|
+
|
|
423
|
+
# Prepare-phase: show gradual progress 0% → 10% while waiting for first step
|
|
424
|
+
local prepare_pct=0
|
|
425
|
+
local prepare_max=70
|
|
426
|
+
local prepare_tick=0
|
|
427
|
+
|
|
428
|
+
# Monitor build progress by watching log for step markers
|
|
429
|
+
while kill -0 "$build_pid" 2>/dev/null; do
|
|
430
|
+
if [[ -f "$build_log" ]]; then
|
|
431
|
+
# Match both legacy "Step N/M" and BuildKit "#N [stage N/M]" formats
|
|
432
|
+
local step_line
|
|
433
|
+
step_line=$(grep -oE '(Step [0-9]+/[0-9]+|#[0-9]+ \[[a-z]+ +[0-9]+/[0-9]+\])' "$build_log" 2>/dev/null | tail -1 || true)
|
|
434
|
+
if [[ -n "$step_line" ]]; then
|
|
435
|
+
# Extract current/total from either format
|
|
436
|
+
local nums
|
|
437
|
+
nums=$(echo "$step_line" | grep -oE '[0-9]+/[0-9]+' | tail -1)
|
|
438
|
+
current_step=${nums%/*}
|
|
439
|
+
total_steps=${nums#*/}
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
if [[ "$current_step" -eq 0 ]]; then
|
|
443
|
+
# Gradually increase from 0% to prepare_max% during prepare phase
|
|
444
|
+
prepare_tick=$(( prepare_tick + 1 ))
|
|
445
|
+
# Increase every ~3 ticks (0.9s) up to prepare_max
|
|
446
|
+
if [[ $(( prepare_tick % 3 )) -eq 0 && "$prepare_pct" -lt "$prepare_max" ]]; then
|
|
447
|
+
prepare_pct=$(( prepare_pct + 1 ))
|
|
448
|
+
fi
|
|
449
|
+
local filled=$(( prepare_pct * bar_width / 100 ))
|
|
450
|
+
local empty=$(( bar_width - filled ))
|
|
451
|
+
local bar="${CYAN}$(printf '%*s' "$filled" '' | tr ' ' '▓')${GRAY}$(printf '%*s' "$empty" '' | tr ' ' '░')${NC}"
|
|
452
|
+
printf "\r ${bar} ${BOLD}${CYAN}%3d%%${NC} ${DIM}Preparing...${NC} " "$prepare_pct"
|
|
453
|
+
elif [[ "$total_steps" -gt 0 ]]; then
|
|
454
|
+
# Map real progress (step/total) into remaining 10%-100% range
|
|
455
|
+
local real_pct=$(( current_step * 100 / total_steps ))
|
|
456
|
+
local pct=$(( prepare_max + real_pct * (100 - prepare_max) / 100 ))
|
|
457
|
+
local filled=$(( pct * bar_width / 100 ))
|
|
458
|
+
local empty=$(( bar_width - filled ))
|
|
459
|
+
# Gradient color: cyan → green → yellow as progress increases
|
|
460
|
+
local bar_color
|
|
461
|
+
if [[ "$pct" -lt 33 ]]; then
|
|
462
|
+
bar_color="${CYAN}"
|
|
463
|
+
elif [[ "$pct" -lt 66 ]]; then
|
|
464
|
+
bar_color="${GREEN}"
|
|
465
|
+
else
|
|
466
|
+
bar_color="${YELLOW}"
|
|
467
|
+
fi
|
|
468
|
+
local bar="${bar_color}$(printf '%*s' "$filled" '' | tr ' ' '▓')${GRAY}$(printf '%*s' "$empty" '' | tr ' ' '░')${NC}"
|
|
469
|
+
printf "\r ${bar} ${BOLD}${bar_color}%3d%%${NC} ${DIM}(%d/%d)${NC} " "$pct" "$current_step" "$total_steps"
|
|
470
|
+
fi
|
|
471
|
+
fi
|
|
472
|
+
sleep 0.3
|
|
473
|
+
done
|
|
474
|
+
|
|
475
|
+
# Final status
|
|
476
|
+
wait "$build_pid"
|
|
477
|
+
local build_exit=$?
|
|
478
|
+
|
|
479
|
+
if [[ "$build_exit" -eq 0 ]]; then
|
|
480
|
+
printf "\r ${GREEN}$(printf '%*s' "$bar_width" '' | tr ' ' '▓')${NC} ${BOLD}${GREEN}100%%${NC} ${DIM}(%d/%d)${NC} \n" "$total_steps" "$total_steps"
|
|
481
|
+
echo ""
|
|
482
|
+
log "Image built successfully ${I_SPARKLE}"
|
|
483
|
+
rm -f "$build_log"
|
|
484
|
+
else
|
|
485
|
+
echo ""
|
|
486
|
+
err "Image build failed!"
|
|
487
|
+
echo ""
|
|
488
|
+
echo -e " ${BOLD}${WHITE}Last 20 lines of build output:${NC}"
|
|
489
|
+
echo ""
|
|
490
|
+
tail -20 "$build_log" | while IFS= read -r line; do
|
|
491
|
+
echo -e " ${DIM}${line}${NC}"
|
|
492
|
+
done
|
|
493
|
+
echo ""
|
|
494
|
+
echo -e " ${DIM}Full build log:${NC} ${WHITE}${build_log}${NC}"
|
|
495
|
+
echo ""
|
|
496
|
+
echo -e " ${BOLD}${WHITE}Common fixes:${NC}"
|
|
497
|
+
echo -e " ${YELLOW}•${NC} Network issue ${DIM}→${NC} Check internet connection"
|
|
498
|
+
echo -e " ${YELLOW}•${NC} Disk full ${DIM}→${NC} Run ${GREEN}docker system prune${NC}"
|
|
499
|
+
echo -e " ${YELLOW}•${NC} Cached layers ${DIM}→${NC} Run ${GREEN}$0 --rebuild${NC}"
|
|
500
|
+
echo ""
|
|
501
|
+
exit 1
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
else
|
|
505
|
+
log "Image ${BOLD}${IMAGE_NAME}:${IMAGE_TAG}${NC} ready ${DIM}(use --rebuild to force)${NC}"
|
|
506
|
+
fi
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# ── Generate firewall script to temp file ─────────────────────
|
|
510
|
+
# Writes firewall script to a temp file to avoid shell injection
|
|
511
|
+
# when embedding in docker run -c commands
|
|
512
|
+
gen_firewall_file() {
|
|
513
|
+
local fw_file="$1"
|
|
514
|
+
# Copy the canonical firewall script and inject extra domains
|
|
515
|
+
cp "${SCRIPT_DIR}/docker/init-firewall.sh" "$fw_file"
|
|
516
|
+
|
|
517
|
+
# Inject extra domains into the ALLOWED_DOMAINS array
|
|
518
|
+
if [[ ${#EXTRA_DOMAINS[@]} -gt 0 ]]; then
|
|
519
|
+
local extra_lines=""
|
|
520
|
+
for d in "${EXTRA_DOMAINS[@]}"; do
|
|
521
|
+
[[ -n "$d" ]] && extra_lines+=" \"$d\"\n"
|
|
522
|
+
done
|
|
523
|
+
if [[ -n "$extra_lines" ]]; then
|
|
524
|
+
# Insert extra domains before the closing paren of ALLOWED_DOMAINS
|
|
525
|
+
local tmp_fw="${fw_file}.tmp"
|
|
526
|
+
awk -v extra="$extra_lines" '/^)$/ && !done { printf "%s", extra; done=1 } { print }' "$fw_file" > "$tmp_fw"
|
|
527
|
+
mv "$tmp_fw" "$fw_file"
|
|
528
|
+
fi
|
|
529
|
+
fi
|
|
530
|
+
|
|
531
|
+
chmod +x "$fw_file"
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
# ── Status box ───────────────────────────────────────────────
|
|
535
|
+
show_launch_box() {
|
|
536
|
+
local mode="$1"
|
|
537
|
+
local box_w=55
|
|
538
|
+
|
|
539
|
+
# Print a row: icon, label, value, value_color
|
|
540
|
+
# Fixed inner width of 55 chars between │ and │
|
|
541
|
+
box_row() {
|
|
542
|
+
local icon="$1" label="$2" value="$3" vcolor="$4"
|
|
543
|
+
local line
|
|
544
|
+
# Build the visible content: " X Label__ Value"
|
|
545
|
+
# icon takes 2 display cols, so we account for that
|
|
546
|
+
line=$(printf " %s %-9s %s" "$icon" "$label" "$value")
|
|
547
|
+
# Calculate visible width (strip ANSI, account for emoji = 2 cols each)
|
|
548
|
+
local visible_len=$(( 2 + 2 + 2 + 9 + 2 + ${#value} )) # emoji=2 display cols
|
|
549
|
+
local pad_len=$(( box_w - visible_len ))
|
|
550
|
+
[[ "$pad_len" -lt 0 ]] && pad_len=0
|
|
551
|
+
printf " ${BOLD}${CYAN}│${NC} %s ${DIM}%-9s${NC} ${vcolor}%s${NC}%*s${BOLD}${CYAN}│${NC}\n" \
|
|
552
|
+
"$icon" "$label" "$value" "$pad_len" ""
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
echo ""
|
|
556
|
+
echo -e " ${BOLD}${CYAN}┌───────────────────────────────────────────────────────┐${NC}"
|
|
557
|
+
echo -e " ${BOLD}${CYAN}│${NC} ${I_BRAIN} ${BOLD}${WHITE}Claude Code Sandbox${NC} ${BOLD}${CYAN}│${NC}"
|
|
558
|
+
echo -e " ${BOLD}${CYAN}├───────────────────────────────────────────────────────┤${NC}"
|
|
559
|
+
echo -e " ${BOLD}${CYAN}│${NC} ${BOLD}${CYAN}│${NC}"
|
|
560
|
+
|
|
561
|
+
box_row "$I_FOLDER" "Project :" "$PROJECT_NAME" "${WHITE}"
|
|
562
|
+
box_row "$RUNTIME_ICON" "Runtime :" "$RUNTIME_LABEL" "${WHITE}"
|
|
563
|
+
box_row "$I_ZAP" "Mode :" "$mode" "${WHITE}"
|
|
564
|
+
|
|
565
|
+
if [[ "$NO_FIREWALL" == "true" ]]; then
|
|
566
|
+
box_row "$I_GLOBE" "Firewall:" "Disabled" "${YELLOW}"
|
|
567
|
+
else
|
|
568
|
+
box_row "$I_LOCK" "Firewall:" "Active" "${GREEN}"
|
|
569
|
+
fi
|
|
570
|
+
|
|
571
|
+
echo -e " ${BOLD}${CYAN}│${NC} ${BOLD}${CYAN}│${NC}"
|
|
572
|
+
echo -e " ${BOLD}${CYAN}└───────────────────────────────────────────────────────┘${NC}"
|
|
573
|
+
echo ""
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
# ── Run container ────────────────────────────────────────────
|
|
577
|
+
run_container() {
|
|
578
|
+
step "Launching container ${I_ROCKET}"
|
|
579
|
+
|
|
580
|
+
# Cleanup existing
|
|
581
|
+
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
582
|
+
docker rm -f "$CONTAINER_NAME" &>/dev/null || true
|
|
583
|
+
fi
|
|
584
|
+
|
|
585
|
+
local RUN_ARGS=(
|
|
586
|
+
--rm -it
|
|
587
|
+
--name "$CONTAINER_NAME"
|
|
588
|
+
--cap-add=NET_ADMIN
|
|
589
|
+
-v "$PROJECT_PATH:/workspace:cached"
|
|
590
|
+
-v "${HOME}/.claude:/home/node/.claude:cached"
|
|
591
|
+
-v "claude-npm:/usr/local/share/npm-global"
|
|
592
|
+
-v "claude-history:/commandhistory"
|
|
593
|
+
-e "CLAUDE_CONFIG_DIR=/home/node/.claude"
|
|
594
|
+
-e "TZ=$TZ"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Mount .gitconfig if exists
|
|
598
|
+
if [[ -f "${HOME}/.gitconfig" ]]; then
|
|
599
|
+
RUN_ARGS+=(-v "${HOME}/.gitconfig:/home/node/.gitconfig:ro")
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
# Generate firewall script as temp file and mount into container
|
|
603
|
+
local FW_SETUP="true"
|
|
604
|
+
if [[ "$NO_FIREWALL" == "false" ]]; then
|
|
605
|
+
local fw_tmp
|
|
606
|
+
# Colima only shares $HOME into its VM — /tmp and $TMPDIR (/var/folders)
|
|
607
|
+
# are not accessible, causing Docker to mount them as empty directories.
|
|
608
|
+
# Place temp file under $HOME to ensure visibility across all runtimes.
|
|
609
|
+
local tmp_base="${HOME}/.cache/cc-sandboxer"
|
|
610
|
+
mkdir -p "$tmp_base"
|
|
611
|
+
fw_tmp=$(mktemp "${tmp_base}/fw-XXXXXXXX")
|
|
612
|
+
gen_firewall_file "$fw_tmp"
|
|
613
|
+
RUN_ARGS+=(-v "$fw_tmp:/opt/init-firewall.sh:ro")
|
|
614
|
+
FW_SETUP="sudo bash /opt/init-firewall.sh"
|
|
615
|
+
fi
|
|
616
|
+
|
|
617
|
+
# Build claude command as an array to avoid injection
|
|
618
|
+
local CLAUDE_CMD="claude --dangerously-skip-permissions"
|
|
619
|
+
if [[ ${#CLAUDE_ARGS[@]} -gt 0 ]]; then
|
|
620
|
+
# Quote each arg individually for safe embedding
|
|
621
|
+
for arg in "${CLAUDE_ARGS[@]}"; do
|
|
622
|
+
CLAUDE_CMD+=" $(printf '%q' "$arg")"
|
|
623
|
+
done
|
|
624
|
+
fi
|
|
625
|
+
|
|
626
|
+
if [[ "$SHELL_ONLY" == "true" ]]; then
|
|
627
|
+
show_launch_box "Shell Only"
|
|
628
|
+
|
|
629
|
+
docker run "${RUN_ARGS[@]}" "$IMAGE_NAME:$IMAGE_TAG" -c "
|
|
630
|
+
${FW_SETUP} 2>/tmp/fw-err.log || { echo -e '\033[1;33m⚠ Firewall failed to initialize.\033[0m Ensure container has --cap-add=NET_ADMIN.'; cat /tmp/fw-err.log; echo ''; }
|
|
631
|
+
echo ''
|
|
632
|
+
echo 'Shell ready! Start Claude manually:'
|
|
633
|
+
echo ''
|
|
634
|
+
echo ' claude --dangerously-skip-permissions'
|
|
635
|
+
echo ' claude --dangerously-skip-permissions -p \"your task\"'
|
|
636
|
+
echo ' claude --dangerously-skip-permissions --continue'
|
|
637
|
+
echo ''
|
|
638
|
+
exec zsh -l
|
|
639
|
+
"
|
|
640
|
+
else
|
|
641
|
+
show_launch_box "Skip Permissions"
|
|
642
|
+
|
|
643
|
+
echo -e " ${DIM}${I_ZAP} ${CLAUDE_CMD}${NC}"
|
|
644
|
+
echo ""
|
|
645
|
+
divider
|
|
646
|
+
echo ""
|
|
647
|
+
|
|
648
|
+
docker run "${RUN_ARGS[@]}" "$IMAGE_NAME:$IMAGE_TAG" -c "
|
|
649
|
+
${FW_SETUP} 2>/tmp/fw-err.log || { echo -e '\033[1;33m⚠ Firewall failed to initialize.\033[0m Ensure container has --cap-add=NET_ADMIN.'; cat /tmp/fw-err.log; echo ''; }
|
|
650
|
+
${CLAUDE_CMD}
|
|
651
|
+
"
|
|
652
|
+
fi
|
|
653
|
+
|
|
654
|
+
# Cleanup temp firewall file
|
|
655
|
+
[[ -f "${fw_tmp:-}" ]] && rm -f "$fw_tmp"
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# ══════════════════════════════════════════════════════════════
|
|
659
|
+
# 🏁 Main
|
|
660
|
+
# ══════════════════════════════════════════════════════════════
|
|
661
|
+
main() {
|
|
662
|
+
show_banner
|
|
663
|
+
detect_runtime
|
|
664
|
+
|
|
665
|
+
echo ""
|
|
666
|
+
build_image
|
|
667
|
+
|
|
668
|
+
echo ""
|
|
669
|
+
run_container
|
|
670
|
+
|
|
671
|
+
# Post-exit
|
|
672
|
+
echo ""
|
|
673
|
+
divider
|
|
674
|
+
echo ""
|
|
675
|
+
success "Session ended. Your project files are safe ${I_SHIELD}"
|
|
676
|
+
echo ""
|
|
677
|
+
echo -e " ${DIM}Run again :${NC} ${GREEN}cc-sandboxer${NC}"
|
|
678
|
+
echo -e " ${DIM}Resume :${NC} ${GREEN}cc-sandboxer --continue${NC}"
|
|
679
|
+
echo ""
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
main
|