Skip to content

Commit 6db0ca4

Browse files
committed
implement overlayfs with post merging for working repo
1 parent 2b158ec commit 6db0ca4

3 files changed

Lines changed: 427 additions & 51 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ There are existing attempts to sandbox agent processes, the most serious I find
1313

1414
# Usage
1515

16+
* requires Bubblewrap 0.11+ for the overlayfs support
1617
* add the script(s) to your PATH
1718
* `mkdir -p ~/aihome`
1819
* agent-dev bash

agent-dev

Lines changed: 63 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
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:
@@ -20,17 +25,25 @@
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

2735
set -euo pipefail
2836

2937
# Configuration
3038
AI_HOME="$HOME/aihome"
3139
REPO_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) =====
3447
PROTECTED_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) =====
5467
PROTECTED_AGENT_HOME_FILES=(
5568
".gitconfig" # Safe config created during setup
5669
".bashrc"
@@ -100,6 +113,9 @@ if [[ ! -d .git ]]; then
100113
exit 1
101114
fi
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)
104120
echo "🔄 Checking for Claude updates via npm..."
105121
npm update -g @anthropics/claude-code 2>&1 | grep -E "(changed|up to date)" || true
@@ -120,97 +136,76 @@ cat > "$CLAUDE_SETTINGS_TEMP" << 'EOF'
120136
}
121137
EOF
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)
140148
RESOLV_CONF_TARGET=$(readlink -f /etc/resolv.conf)
141149

142150
# Build bwrap arguments for protected repo paths
143151
BWRAP_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)
146154
for 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
153158
done
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)
156161
for 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
163165
done
164166

165167
# Build bwrap arguments for whitelisted real home paths
166168
BWRAP_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
169171
for 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
176175
done
177176

178-
# Whitelisted directories from real home: ro-bind if exists, tmpfs if not
177+
# Whitelisted directories from real home: ro-bind if exists
179178
for 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
186182
done
187183

188184
# Build bwrap arguments for protected agent home paths
189185
BWRAP_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
192188
for 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
199192
done
200193

201-
# Protected directories in agent home: ro-bind if exists, tmpfs if not
194+
# Protected directories in agent home: ro-bind if exists
202195
for 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
209199
done
210200

211201
echo "🔒 Starting Claude Code in isolated environment..."
212202
echo " 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"
214209
echo ""
215210

216211
# Count protections by category
@@ -219,12 +214,13 @@ AGENT_HOME_PROTECTED=$((${#BWRAP_AGENT_HOME_PROTECTIONS[@]}))
219214
WHITELIST_COUNT=$((${#BWRAP_HOME_WHITELIST[@]}))
220215

221216
echo "🛡️ Protection summary:"
222-
echo " • Repo: $REPO_PROTECTED paths (ro-bind, tmpfs, or /dev/null)"
217+
echo " • Repo: $REPO_PROTECTED existing paths (read-only)"
223218
echo " • Agent home: $AGENT_HOME_PROTECTED config files (read-only)"
224219
echo " • Real home: $WHITELIST_COUNT dev tools (read-only)"
225220
echo ""
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"
228224
echo ""
229225

230226
bwrap \
@@ -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

Comments
 (0)