Skip to content

Commit 2b158ec

Browse files
committed
initial agent version where repo is not solidly protected agains modifying ignored files
1 parent 905047f commit 2b158ec

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ build-iPhoneSimulator/
5151

5252
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
5353
.rvmrc
54+
.tool-versions
5455

5556
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
5657
# .rubocop-https?--*

agent-dev

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
#!/bin/bash
2+
# claude-code-isolated: Secure bubblewrap wrapper for running commands in isolation
3+
#
4+
# Provides isolated environment with:
5+
# - ~/aihome mounted at your actual $HOME path (mostly writable)
6+
# - Current repo mounted at its original absolute path (read-write)
7+
# - All paths preserved to avoid hardcoded path issues
8+
# - No access to sensitive files (~/.ssh, ~/.aws, ~/.gnupg, etc.)
9+
#
10+
# Setup:
11+
# 1. Create isolated home: mkdir -p ~/aihome
12+
# 2. Login to Claude in the sandbox:
13+
# cd /path/to/your/repo
14+
# claude-code-isolated bash
15+
# claude # Follow login prompts
16+
# exit
17+
# 3. (Optional) Disable auto-updates in isolated home:
18+
# mkdir -p ~/aihome/.claude
19+
# echo '{"autoUpdates": false}' > ~/aihome/.claude/settings.local.json
20+
#
21+
# Usage:
22+
# cd /path/to/your/repo
23+
# claude-code-isolated # Runs claude
24+
# claude-code-isolated bash # Inspect the environment
25+
# claude-code-isolated vim file # Run any command
26+
27+
set -euo pipefail
28+
29+
# Configuration
30+
AI_HOME="$HOME/aihome"
31+
REPO_DIR="$(pwd)"
32+
33+
# ===== Protected paths in repository (read-only or tmpfs/devnull if non-existent) =====
34+
PROTECTED_REPO_FILES=(
35+
".git/config"
36+
".git/info/exclude" # Prevent sneaking ignore rules (not visible in git status)
37+
".gitconfig"
38+
".gitmodules"
39+
".ripgreprc"
40+
".project"
41+
".classpath"
42+
)
43+
44+
PROTECTED_REPO_DIRS=(
45+
".git/hooks"
46+
".vscode"
47+
".idea"
48+
".settings"
49+
".claude/commands"
50+
".claude/agents"
51+
)
52+
53+
# ===== Protected paths in agent home (read-only overlays on writable home) =====
54+
PROTECTED_AGENT_HOME_FILES=(
55+
".gitconfig" # Safe config created during setup
56+
".bashrc"
57+
".bash_profile"
58+
".zshrc"
59+
".zprofile"
60+
".profile"
61+
".npmrc"
62+
".gemrc"
63+
".inputrc"
64+
)
65+
66+
PROTECTED_AGENT_HOME_DIRS=(
67+
".ssh" # If accidentally created, keep read-only
68+
".bashrc.d" # Bash config scripts sourced by .bashrc
69+
".zshrc.d" # Zsh config scripts (if used)
70+
)
71+
72+
# ===== Whitelisted paths from real home (read-only mounts) =====
73+
WHITELISTED_HOME_FILES=(
74+
# Add files from real home that should be readable (if any)
75+
)
76+
77+
WHITELISTED_HOME_DIRS=(
78+
".asdf"
79+
".npm"
80+
".bundle"
81+
".gem"
82+
".cargo"
83+
".rustup"
84+
".local"
85+
"bin"
86+
)
87+
88+
# Validate environment
89+
if [[ ! -d "$AI_HOME" ]]; then
90+
echo "⚠️ AI home directory does not exist: $AI_HOME"
91+
echo ""
92+
echo "This directory will be used as Claude Code's isolated home."
93+
echo "Create it with: mkdir -p $AI_HOME"
94+
echo ""
95+
exit 1
96+
fi
97+
98+
if [[ ! -d .git ]]; then
99+
echo "Error: Must run from a git repository root"
100+
exit 1
101+
fi
102+
103+
# Update Claude before entering sandbox (via npm, not trusting claude update)
104+
echo "🔄 Checking for Claude updates via npm..."
105+
npm update -g @anthropics/claude-code 2>&1 | grep -E "(changed|up to date)" || true
106+
echo ""
107+
108+
# Prepare Claude settings: create permissive default in temp file
109+
CLAUDE_SETTINGS_TEMP=$(mktemp)
110+
111+
cat > "$CLAUDE_SETTINGS_TEMP" << 'EOF'
112+
{
113+
"autoApprove": {
114+
"bash": ["*"],
115+
"read": ["*"],
116+
"write": ["*"],
117+
"edit": ["*"]
118+
},
119+
"autoUpdates": false
120+
}
121+
EOF
122+
123+
# Cleanup function: remove mount point artifacts and temp files
124+
cleanup_protected_files() {
125+
# Remove zero-length protected files (mount point artifacts)
126+
for file in "${PROTECTED_REPO_FILES[@]}"; do
127+
if [[ ! -s "$REPO_DIR/$file" ]]; then
128+
rm -f "$REPO_DIR/$file"
129+
fi
130+
done
131+
132+
# Remove temp Claude settings
133+
rm -f "$CLAUDE_SETTINGS_TEMP"
134+
}
135+
136+
# Register cleanup on exit (handles normal exit, errors, and interrupts)
137+
trap cleanup_protected_files EXIT
138+
139+
# Resolve DNS configuration (handles symlinks like /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf)
140+
RESOLV_CONF_TARGET=$(readlink -f /etc/resolv.conf)
141+
142+
# Build bwrap arguments for protected repo paths
143+
BWRAP_REPO_PROTECTIONS=()
144+
145+
# Protected files: ro-bind if exists, bind /dev/null if not (prevents creation)
146+
for file in "${PROTECTED_REPO_FILES[@]}"; do
147+
if [[ -f "$REPO_DIR/$file" ]]; then
148+
BWRAP_REPO_PROTECTIONS+=(--ro-bind "$REPO_DIR/$file" "$REPO_DIR/$file")
149+
else
150+
# Bind /dev/null to prevent malicious file creation (creates empty mount point)
151+
BWRAP_REPO_PROTECTIONS+=(--ro-bind /dev/null "$REPO_DIR/$file")
152+
fi
153+
done
154+
155+
# Protected directories: ro-bind if exists, tmpfs if not (prevents creation)
156+
for dir in "${PROTECTED_REPO_DIRS[@]}"; do
157+
if [[ -d "$REPO_DIR/$dir" ]]; then
158+
BWRAP_REPO_PROTECTIONS+=(--ro-bind "$REPO_DIR/$dir" "$REPO_DIR/$dir")
159+
else
160+
# Mount empty tmpfs to prevent malicious creation
161+
BWRAP_REPO_PROTECTIONS+=(--tmpfs "$REPO_DIR/$dir")
162+
fi
163+
done
164+
165+
# Build bwrap arguments for whitelisted real home paths
166+
BWRAP_HOME_WHITELIST=()
167+
168+
# Whitelisted files from real home: ro-bind if exists, /dev/null if not
169+
for file in "${WHITELISTED_HOME_FILES[@]}"; do
170+
if [[ -f "$HOME/$file" ]]; then
171+
BWRAP_HOME_WHITELIST+=(--ro-bind "$HOME/$file" "$HOME/$file")
172+
else
173+
# Prevent creation in agent home
174+
BWRAP_HOME_WHITELIST+=(--ro-bind /dev/null "$HOME/$file")
175+
fi
176+
done
177+
178+
# Whitelisted directories from real home: ro-bind if exists, tmpfs if not
179+
for dir in "${WHITELISTED_HOME_DIRS[@]}"; do
180+
if [[ -d "$HOME/$dir" ]]; then
181+
BWRAP_HOME_WHITELIST+=(--ro-bind "$HOME/$dir" "$HOME/$dir")
182+
else
183+
# Prevent persistent creation in agent home (writes go to RAM)
184+
BWRAP_HOME_WHITELIST+=(--tmpfs "$HOME/$dir")
185+
fi
186+
done
187+
188+
# Build bwrap arguments for protected agent home paths
189+
BWRAP_AGENT_HOME_PROTECTIONS=()
190+
191+
# Protected files in agent home: ro-bind if exists, /dev/null if not
192+
for file in "${PROTECTED_AGENT_HOME_FILES[@]}"; do
193+
if [[ -f "$AI_HOME/$file" ]]; then
194+
BWRAP_AGENT_HOME_PROTECTIONS+=(--ro-bind "$AI_HOME/$file" "$HOME/$file")
195+
else
196+
# Prevent malicious creation for persistence attacks
197+
BWRAP_AGENT_HOME_PROTECTIONS+=(--ro-bind /dev/null "$HOME/$file")
198+
fi
199+
done
200+
201+
# Protected directories in agent home: ro-bind if exists, tmpfs if not
202+
for dir in "${PROTECTED_AGENT_HOME_DIRS[@]}"; do
203+
if [[ -d "$AI_HOME/$dir" ]]; then
204+
BWRAP_AGENT_HOME_PROTECTIONS+=(--ro-bind "$AI_HOME/$dir" "$HOME/$dir")
205+
else
206+
# Prevent persistent creation for persistence attacks (writes go to RAM)
207+
BWRAP_AGENT_HOME_PROTECTIONS+=(--tmpfs "$HOME/$dir")
208+
fi
209+
done
210+
211+
echo "🔒 Starting Claude Code in isolated environment..."
212+
echo " Home: $HOME$AI_HOME (isolated, mostly writable)"
213+
echo " Repo: $REPO_DIR (read-write, with protections)"
214+
echo ""
215+
216+
# Count protections by category
217+
REPO_PROTECTED=$((${#PROTECTED_REPO_FILES[@]} + ${#PROTECTED_REPO_DIRS[@]}))
218+
AGENT_HOME_PROTECTED=$((${#BWRAP_AGENT_HOME_PROTECTIONS[@]}))
219+
WHITELIST_COUNT=$((${#BWRAP_HOME_WHITELIST[@]}))
220+
221+
echo "🛡️ Protection summary:"
222+
echo " • Repo: $REPO_PROTECTED paths (ro-bind, tmpfs, or /dev/null)"
223+
echo " • Agent home: $AGENT_HOME_PROTECTED config files (read-only)"
224+
echo " • Real home: $WHITELIST_COUNT dev tools (read-only)"
225+
echo ""
226+
echo "💡 Non-existent paths use tmpfs/devnull to prevent creation"
227+
echo "💡 To restore: rm -rf $AI_HOME && re-run this script"
228+
echo ""
229+
230+
bwrap \
231+
`# ===== System directories (read-only) =====` \
232+
--ro-bind /usr /usr \
233+
--ro-bind /etc /etc \
234+
--ro-bind-try /opt /opt \
235+
--ro-bind-try /nix /nix \
236+
\
237+
`# Symlinks for compatibility` \
238+
--symlink /usr/lib /lib \
239+
--symlink /usr/lib64 /lib64 \
240+
--symlink /usr/bin /bin \
241+
--symlink /usr/sbin /sbin \
242+
\
243+
`# ===== Pseudo-filesystems =====` \
244+
--proc /proc \
245+
--dev /dev \
246+
--tmpfs /run \
247+
--tmpfs /sys \
248+
\
249+
`# ===== DNS resolution =====` \
250+
`# Mount the actual resolv.conf file at its real location (handles /etc/resolv.conf -> /run/systemd/... symlinks)` \
251+
--ro-bind "$RESOLV_CONF_TARGET" "$RESOLV_CONF_TARGET" \
252+
\
253+
`# ===== Temporary directories (isolated, writable) =====` \
254+
--tmpfs /tmp \
255+
--tmpfs /var/tmp \
256+
\
257+
`# ===== AI Home mounted at real home path (WRITABLE) =====` \
258+
`# This ensures hardcoded paths to $HOME work correctly` \
259+
`# Agent can write anywhere here - easy to restore if corrupted` \
260+
--bind "$AI_HOME" "$HOME" \
261+
\
262+
`# ===== Protected agent home paths (read-only overlays) =====` \
263+
`# Config files created during setup - prevent tampering/persistence attacks` \
264+
"${BWRAP_AGENT_HOME_PROTECTIONS[@]}" \
265+
\
266+
`# ===== Whitelisted real home paths (read-only overlays) =====` \
267+
`# Dev tools and files from real home mounted read-only` \
268+
"${BWRAP_HOME_WHITELIST[@]}" \
269+
\
270+
`# ===== Current repository at original path (read-write) =====` \
271+
`# If under $HOME, this overlays the aihome mount at this specific location` \
272+
--bind "$REPO_DIR" "$REPO_DIR" \
273+
--chdir "$REPO_DIR" \
274+
\
275+
`# ===== Protected repo paths (read-only or tmpfs) =====` \
276+
`# Files: ro-bind if exists, ignored if not` \
277+
`# Dirs: ro-bind if exists, tmpfs if not (prevents malicious creation)` \
278+
"${BWRAP_REPO_PROTECTIONS[@]}" \
279+
\
280+
`# ===== Claude settings override (permissive, non-persistent) =====` \
281+
`# Writable temp file overlays repo settings - changes don't persist to host` \
282+
--bind "$CLAUDE_SETTINGS_TEMP" "$REPO_DIR/.claude/settings.local.json" \
283+
\
284+
`# ===== Isolation and security =====` \
285+
--unshare-all \
286+
--share-net \
287+
--die-with-parent \
288+
--new-session \
289+
\
290+
`# ===== Environment variables =====` \
291+
`# --setenv HOME "$HOME"` \
292+
`# --setenv USER "$(whoami)"` \
293+
`# --setenv LOGNAME "$(whoami)"` \
294+
`# --setenv SHELL "/bin/bash"` \
295+
`# --setenv TERM "${TERM:-xterm-256color}"` \
296+
`# --setenv PATH "$HOME/.asdf/shims:$HOME/.asdf/bin:$HOME/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"` \
297+
`# --setenv ASDF_DIR "${HOME}/.asdf"` \
298+
`# --setenv ASDF_DATA_DIR "${HOME}/.asdf"` \
299+
\
300+
`# Clean environment - remove potentially dangerous vars` \
301+
--unsetenv DBUS_SESSION_BUS_ADDRESS \
302+
--unsetenv XDG_RUNTIME_DIR \
303+
--unsetenv SSH_AUTH_SOCK \
304+
--unsetenv GPG_AGENT_INFO \
305+
\
306+
`# ===== Execute command (defaults to claude) =====` \
307+
"${@:-claude}"

0 commit comments

Comments
 (0)