33#
44# Provides isolated environment with:
55# - ~/aihome mounted at your actual $HOME path (mostly writable)
6- # - Current repo mounted at its original absolute path (read-write )
6+ # - Current repo mounted with overlayfs (changes isolated for review )
77# - All paths preserved to avoid hardcoded path issues
88# - No access to sensitive files (~/.ssh, ~/.aws, ~/.gnupg, etc.)
99#
10+ # Overlay security model:
11+ # - Repository changes are written to ~/aihome/.overlay/<repo-full-path>/upper
12+ # - Original repository remains untouched (read-only lower layer)
13+ # - After session, review and merge changes with: ./merge-overlay
14+ #
1015# Setup:
1116# 1. Create isolated home: mkdir -p ~/aihome
1217# 2. Login to Claude in the sandbox:
1318# cd /path/to/your/repo
14- # claude-code-isolated bash
19+ # ./agent-dev bash
1520# claude # Follow login prompts
1621# exit
1722# 3. (Optional) Disable auto-updates in isolated home:
2025#
2126# Usage:
2227# 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
28+ # ./agent-dev # Runs claude
29+ # ./agent-dev bash # Inspect the environment
30+ # ./agent-dev vim file # Run any command
31+ #
32+ # After session - review changes:
33+ # ./merge-overlay /path/to/repo ~/aihome/.overlay/path/to/repo/upper
2634
2735set -euo pipefail
2836
2937# Configuration
3038AI_HOME=" $HOME /aihome"
3139REPO_DIR=" $( pwd) "
3240
33- # ===== Protected paths in repository (read-only or tmpfs/devnull if non-existent) =====
41+ # Overlay directories for repository isolation (using full path to avoid conflicts)
42+ OVERLAY_BASE=" $AI_HOME /.overlay$( realpath " $REPO_DIR " ) "
43+ OVERLAY_UPPER=" $OVERLAY_BASE /upper"
44+ OVERLAY_WORK=" $OVERLAY_BASE /work"
45+
46+ # ===== Protected paths in repository (read-only if exist, defense-in-depth) =====
3447PROTECTED_REPO_FILES=(
3548 " .git/config"
3649 " .git/info/exclude" # Prevent sneaking ignore rules (not visible in git status)
@@ -50,7 +63,7 @@ PROTECTED_REPO_DIRS=(
5063 " .claude/agents"
5164)
5265
53- # ===== Protected paths in agent home (read-only overlays on writable home ) =====
66+ # ===== Protected paths in agent home (read-only if exist ) =====
5467PROTECTED_AGENT_HOME_FILES=(
5568 " .gitconfig" # Safe config created during setup
5669 " .bashrc"
@@ -100,6 +113,9 @@ if [[ ! -d .git ]]; then
100113 exit 1
101114fi
102115
116+ # Create overlay directories for repository isolation
117+ mkdir -p " $OVERLAY_UPPER " " $OVERLAY_WORK "
118+
103119# Update Claude before entering sandbox (via npm, not trusting claude update)
104120echo " 🔄 Checking for Claude updates via npm..."
105121npm update -g @anthropics/claude-code 2>&1 | grep -E " (changed|up to date)" || true
@@ -120,97 +136,76 @@ cat > "$CLAUDE_SETTINGS_TEMP" << 'EOF'
120136}
121137EOF
122138
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
139+ # Cleanup function: remove temp files
140+ cleanup () {
133141 rm -f " $CLAUDE_SETTINGS_TEMP "
134142}
135143
136144# Register cleanup on exit (handles normal exit, errors, and interrupts)
137- trap cleanup_protected_files EXIT
145+ trap cleanup EXIT
138146
139147# Resolve DNS configuration (handles symlinks like /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf)
140148RESOLV_CONF_TARGET=$( readlink -f /etc/resolv.conf)
141149
142150# Build bwrap arguments for protected repo paths
143151BWRAP_REPO_PROTECTIONS=()
144152
145- # Protected files: ro-bind if exists, bind /dev/null if not (prevents creation)
153+ # Protected files: ro-bind if exists (overlayfs will catch any creation attempts in upper )
146154for file in " ${PROTECTED_REPO_FILES[@]} " ; do
147155 if [[ -f " $REPO_DIR /$file " ]]; then
148156 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 " )
152157 fi
153158done
154159
155- # Protected directories: ro-bind if exists, tmpfs if not (prevents creation)
160+ # Protected directories: ro-bind if exists (overlayfs will catch any creation attempts in upper )
156161for dir in " ${PROTECTED_REPO_DIRS[@]} " ; do
157162 if [[ -d " $REPO_DIR /$dir " ]]; then
158163 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 " )
162164 fi
163165done
164166
165167# Build bwrap arguments for whitelisted real home paths
166168BWRAP_HOME_WHITELIST=()
167169
168- # Whitelisted files from real home: ro-bind if exists, /dev/null if not
170+ # Whitelisted files from real home: ro-bind if exists
169171for file in " ${WHITELISTED_HOME_FILES[@]} " ; do
170172 if [[ -f " $HOME /$file " ]]; then
171173 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 " )
175174 fi
176175done
177176
178- # Whitelisted directories from real home: ro-bind if exists, tmpfs if not
177+ # Whitelisted directories from real home: ro-bind if exists
179178for dir in " ${WHITELISTED_HOME_DIRS[@]} " ; do
180179 if [[ -d " $HOME /$dir " ]]; then
181180 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 " )
185181 fi
186182done
187183
188184# Build bwrap arguments for protected agent home paths
189185BWRAP_AGENT_HOME_PROTECTIONS=()
190186
191- # Protected files in agent home: ro-bind if exists, /dev/null if not
187+ # Protected files in agent home: ro-bind if exists
192188for file in " ${PROTECTED_AGENT_HOME_FILES[@]} " ; do
193189 if [[ -f " $AI_HOME /$file " ]]; then
194190 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 " )
198191 fi
199192done
200193
201- # Protected directories in agent home: ro-bind if exists, tmpfs if not
194+ # Protected directories in agent home: ro-bind if exists
202195for dir in " ${PROTECTED_AGENT_HOME_DIRS[@]} " ; do
203196 if [[ -d " $AI_HOME /$dir " ]]; then
204197 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 " )
208198 fi
209199done
210200
211201echo " 🔒 Starting Claude Code in isolated environment..."
212202echo " Home: $HOME → $AI_HOME (isolated, mostly writable)"
213- echo " Repo: $REPO_DIR (read-write, with protections)"
203+ echo " Repo: $REPO_DIR (overlayfs - changes go to overlay)"
204+ echo " "
205+ echo " 📂 Overlay directories:"
206+ echo " Lower: $REPO_DIR (original, read-only)"
207+ echo " Upper: $OVERLAY_UPPER "
208+ echo " Work: $OVERLAY_WORK "
214209echo " "
215210
216211# Count protections by category
@@ -219,12 +214,13 @@ AGENT_HOME_PROTECTED=$((${#BWRAP_AGENT_HOME_PROTECTIONS[@]}))
219214WHITELIST_COUNT=$(( ${# BWRAP_HOME_WHITELIST[@]} ))
220215
221216echo " 🛡️ Protection summary:"
222- echo " • Repo: $REPO_PROTECTED paths (ro-bind, tmpfs, or /dev/null )"
217+ echo " • Repo: $REPO_PROTECTED existing paths (read-only )"
223218echo " • Agent home: $AGENT_HOME_PROTECTED config files (read-only)"
224219echo " • Real home: $WHITELIST_COUNT dev tools (read-only)"
225220echo " "
226- echo " 💡 Non-existent paths use tmpfs/devnull to prevent creation"
227- echo " 💡 To restore: rm -rf $AI_HOME && re-run this script"
221+ echo " 💡 All repo changes isolated in overlay (review with merge-overlay)"
222+ echo " 💡 After session: merge-overlay will run automatically"
223+ echo " 💡 To restore home: rm -rf $AI_HOME "
228224echo " "
229225
230226bwrap \
@@ -267,14 +263,16 @@ bwrap \
267263 ` # Dev tools and files from real home mounted read-only` \
268264 " ${BWRAP_HOME_WHITELIST[@]} " \
269265 \
270- ` # ===== Current repository at original path (read-write) =====` \
266+ ` # ===== Current repository with overlayfs (isolated writes) =====` \
267+ ` # Lower: original repo (read-only), Upper: changes go to overlay, Work: overlayfs internal` \
271268 ` # If under $HOME, this overlays the aihome mount at this specific location` \
272- --bind " $REPO_DIR " " $REPO_DIR " \
269+ --overlay-src " $REPO_DIR " \
270+ --overlay " $OVERLAY_UPPER " " $OVERLAY_WORK " " $REPO_DIR " \
273271 --chdir " $REPO_DIR " \
274272 \
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) ` \
273+ ` # ===== Protected repo paths (read-only defense-in-depth ) =====` \
274+ ` # Existing protected files/dirs are ro-bind (extra layer over overlayfs) ` \
275+ ` # Non-existent paths can be created in overlay and rejected during merge ` \
278276 " ${BWRAP_REPO_PROTECTIONS[@]} " \
279277 \
280278 ` # ===== Claude settings override (permissive, non-persistent) =====` \
@@ -305,3 +303,17 @@ bwrap \
305303 \
306304 ` # ===== Execute command (defaults to claude) =====` \
307305 " ${@:- claude} "
306+
307+ # After bwrap exits, run merge-overlay to review changes
308+ echo " "
309+ echo " ============================================"
310+ echo " Session ended - reviewing changes"
311+ echo " ============================================"
312+ echo " "
313+
314+ # Check if there are any changes in overlay
315+ if [[ -n " $( ls -A " $OVERLAY_UPPER " 2> /dev/null) " ]]; then
316+ merge-overlay " $REPO_DIR " " $OVERLAY_UPPER "
317+ else
318+ echo " ✓ No changes in overlay - nothing to merge"
319+ fi
0 commit comments