| name | skill-sandbox |
| description | Test Claude Code skills in isolated tmux sandboxes. Spawn multiple sessions, verify hook behavior, run integration tests. Use when asked to "test a skill", "run skill tests", "sandbox test", or "/skill-sandbox". |
Skill Testing Sandbox (/skill-sandbox)
Test Claude Code skills in security-isolated tmux sessions that cannot access real credentials, corrupt production state, or accidentally deploy.
CRITICAL SECURITY ARCHITECTURE
+====================================================================+
| REAL ENVIRONMENT (PROTECTED) |
+====================================================================+
| ~/.claude/ |
| .credentials-export.json <- CONTAINS REAL OAUTH TOKENS |
| settings.json <- HOOK CONFIGURATION |
| {skill}-state.json <- ACTIVE SESSION STATE |
| projects/ <- SESSION LOGS WITH API KEYS |
+====================================================================+
| SECURITY BOUNDARY |
+====================================================================+
| SANDBOX ENVIRONMENT (per session) |
+====================================================================+
| /tmp/claude-sandbox-{id}/ |
| fake-home/ |
| .claude/ |
| .credentials-export.json <- FAKE TOKENS (won't work) |
| settings.json <- COPIED (hooks enabled) |
| project/ <- GIT WORKTREE |
| bin/ <- MOCK COMMANDS (gh, az) |
| blocked.log <- BLOCKED COMMAND LOG |
| tmux -L sandbox-{id} <- SEPARATE TMUX SERVER |
| $HOME=/tmp/claude-sandbox-{id}/fake-home |
+====================================================================+
Security Threat Model
| Threat Vector | Real Risk | Mitigation |
|---|
| OAuth token theft | ~/.claude/.credentials-export.json contains real sk-ant-oat01-* tokens | Sandbox uses fake HOME; real file inaccessible |
| State file corruption | ~/.claude/{skill}-state.json shared globally | Isolated $HOME prevents real state access |
| Accidental production deploy | gh workflow run deploy.yml -f environment=production | Mock gh blocks workflow commands |
| Azure infrastructure changes | az containerapp update --name aca-*-prod | Mock az blocks containerapp commands |
| Environment variable leakage | tmux server shares env across sessions | Separate tmux server via -L sandbox-{id} |
| Git operation conflicts | Commits to real branches affect production | Git worktree creates isolated branch |
| Session interference | Multiple tests share state files | Unique sandbox ID per test run |
What IS Protected
- OAuth Credentials - Fake
~/.claude/.credentials-export.json with non-functional tokens
- State Files - Isolated
$HOME means all state writes go to sandbox
- Production Deployments - Mock
gh and az commands block dangerous operations
- Environment Variables - Separate tmux server prevents env leakage
- Git Branches - Worktree isolation keeps test commits separate
What is NOT Protected (Use Caution)
| Risk | Why Not Protected | Recommendation |
|---|
| Network access | macOS lacks namespace isolation | Test with mock APIs or network-safe repos |
| Filesystem outside sandbox | No chroot on macOS | Skills should use relative paths |
| Real API calls | No network interception | Use --model haiku to minimize costs |
| Malicious skill code | Not a security boundary | Review skills before testing |
| Other processes | Same UID as real sessions | Don't run on shared systems |
Evidence: Why This Architecture
tmux Environment Variable Leakage
From Be Careful Using tmux and Environment Variables:
"As long as the tmux server is running, it will retain the copy of the environment at the moment it was started"
Solution: Use -L socket-name to create a separate tmux server per sandbox.
Credentials File Discovery
Real OAuth tokens found at ~/.claude/.credentials-export.json:
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"expiresAt": 1769646245095,
"scopes": ["user:inference", "user:sessions:claude_code"]
}
}
Solution: Create fake credentials file in sandbox HOME.
State File Race Conditions
Global state files found at:
~/.claude/appfix-state.json
~/.claude/melt-state.json
~/.claude/worktree-state.json
From hooks/_common.py:
user_state_path = Path.home() / ".claude" / "appfix-state.json"
Solution: Override $HOME so Path.home() resolves to sandbox directory.
Platform Capabilities (Evidence-Based)
What Works
| Capability | Method | Evidence |
|---|
| Programmatic execution | claude -p "prompt" | Official headless docs |
| Session resumption | --resume <session-id> | Session management |
| Permission bypass | --dangerously-skip-permissions | YOLO mode guide |
| JSON output | --output-format json | Captures session_id, result, usage |
| Git worktree isolation | ~/.claude/hooks/worktree-manager.py | Existing toolkit infrastructure |
| tmux session control | tmux send-keys / tmux capture-pane | Existing test-e2e-tmux.sh |
Known Limitations
| Limitation | Root Cause | Workaround |
|---|
| Raw mode stdin error | Ink (React terminal UI) requires raw mode | Use claude -p "prompt" NOT piping to stdin |
| Hooks captured at startup | Design decision for performance | Restart session to pick up hook changes |
| Session resumption bugs | GitHub #12730 | Use --no-session-persistence for tests |
| Context overflow in headless | GitHub #13831 | Monitor output size, keep prompts focused |
Architecture
PARENT SESSION (this skill)
├── Creates test sandboxes (temp dirs or git worktrees)
├── Spawns child Claude sessions via:
│ ├── Headless mode: claude -p (for automated tests)
│ └── tmux sessions: tmux send-keys (for interactive tests)
├── Monitors execution through:
│ ├── JSON output parsing (headless)
│ ├── tmux capture-pane (interactive)
│ └── File artifact verification (.claude/ state files)
└── Reports results and cleans up
SECURE WORKFLOW (Use This)
Quick Start: Test a Skill Securely
~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh create
SANDBOX_ID="sandbox-1706438445-a1b2c3d4"
tmux -L "sandbox-$SANDBOX_ID" new-session -s test
source /tmp/claude-sandboxes/$SANDBOX_ID/env.sh
cd $SANDBOX_PROJECT
claude --dangerously-skip-permissions
/melt Add a health check endpoint
One-Liner (Full Secure Test)
SANDBOX_ID=$(~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh create 2>/dev/null | tail -1) && \
tmux -L "sandbox-$SANDBOX_ID" new-session -s test \
"source /tmp/claude-sandboxes/$SANDBOX_ID/env.sh && \
cd \$SANDBOX_PROJECT && \
claude --dangerously-skip-permissions; exec bash"
Cleanup
~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh destroy sandbox-1706438445-a1b2c3d4
~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh list
Verify Security
After starting a sandbox session, verify isolation:
echo $HOME
cat ~/.claude/.credentials-export.json | jq '.claudeAiOauth.accessToken'
gh workflow run deploy.yml
cat $SANDBOX_DIR/blocked-commands.log
Test Modes
SECURITY WARNING: The basic patterns below do NOT include credential isolation.
For secure testing, use the sandbox-setup.sh script shown above, which:
- Creates a fake HOME directory (protects
~/.claude/.credentials-export.json)
- Uses a separate tmux server (
-L flag) for environment isolation
- Installs mock
gh and az commands to block dangerous operations
1. Headless Mode (Fast, Automated) - SECURE
Best for CI/CD and rapid iteration. Uses sandbox for security.
SANDBOX_ID=$(~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh create 2>/dev/null | tail -1)
SANDBOX_ROOT="/tmp/claude-sandboxes/$SANDBOX_ID"
env HOME="$SANDBOX_ROOT/fake-home" \
PATH="$SANDBOX_ROOT/bin:$PATH" \
SANDBOX_MODE=true \
SANDBOX_ID="$SANDBOX_ID" \
claude -p "Run /appfix on this repo" \
--dangerously-skip-permissions \
--no-session-persistence \
--output-format json \
--model haiku \
--timeout 120
~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh destroy "$SANDBOX_ID"
1b. Headless Mode (UNSECURED - Use with Caution)
claude -p "Run /appfix on this repo" \
--dangerously-skip-permissions \
--no-session-persistence \
--output-format json \
--model haiku \
--timeout 120
Capture session ID for inspection:
SESSION_ID=$(claude -p "Start skill test" --output-format json | jq -r '.session_id')
echo "Session: $SESSION_ID"
2. tmux Mode (Interactive, Observable) - SECURE
Best for debugging and manual observation. Uses SEPARATE tmux server for environment isolation.
SANDBOX_ID=$(~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh create 2>/dev/null | tail -1)
SANDBOX_ROOT="/tmp/claude-sandboxes/$SANDBOX_ID"
tmux -L "sandbox-$SANDBOX_ID" new-session -d -s test -x 200 -y 50
tmux -L "sandbox-$SANDBOX_ID" send-keys -t test \
"source $SANDBOX_ROOT/env.sh" Enter
sleep 1
tmux -L "sandbox-$SANDBOX_ID" send-keys -t test \
"cd \$SANDBOX_PROJECT" Enter
sleep 1
tmux -L "sandbox-$SANDBOX_ID" send-keys -t test \
"claude --dangerously-skip-permissions" Enter
sleep 5
tmux -L "sandbox-$SANDBOX_ID" send-keys -t test "/appfix" Enter
tmux -L "sandbox-$SANDBOX_ID" attach -t test
~/.claude/skills/skill-sandbox/scripts/sandbox-setup.sh destroy "$SANDBOX_ID"
2b. tmux Mode (UNSECURED - Use with Caution)
SESSION_NAME="skill-test-$(date +%s)"
tmux new-session -d -s "$SESSION_NAME" -x 200 -y 50
tmux send-keys -t "$SESSION_NAME" "cd /tmp/skill-test-dir" Enter
tmux send-keys -t "$SESSION_NAME" "claude --dangerously-skip-permissions" Enter
3. Python SDK Mode (Programmatic Control)
Best for complex test orchestration with hooks and callbacks.
from claude_agent_sdk import query, ClaudeAgentOptions
async def test_skill():
options = ClaudeAgentOptions(
permission_mode='bypassPermissions',
cwd="/tmp/test-sandbox",
max_turns=10,
model="haiku"
)
async for message in query(
prompt="Run /build to implement a simple hello world",
options=options
):
print(message)
Workflow
Phase 1: Create Test Sandbox
TEST_DIR=$(mktemp -d "/tmp/skill-test-XXXXXX")
cd "$TEST_DIR"
git init && git commit --allow-empty -m "init"
python3 ~/.claude/hooks/worktree-manager.py create "test-$(date +%s)"
Phase 2: Configure Test State
Create state files to trigger specific skill behaviors:
mkdir -p .claude
cat > .claude/appfix-state.json << 'EOF'
{
"iteration": 1,
"started_at": "2026-01-28T10:00:00Z",
"plan_mode_completed": false,
"parallel_mode": false,
"coordinator": true,
"services": {},
"fixes_applied": [],
"verification_evidence": null
}
EOF
Phase 3: Execute Test
timeout 120 claude -p "Your test prompt here" \
--dangerously-skip-permissions \
--no-session-persistence \
--output-format json \
2>"$TEST_DIR/stderr.log" \
>"$TEST_DIR/stdout.log"
jq '.result' "$TEST_DIR/stdout.log"
Phase 4: Verify Outcomes
cat .claude/appfix-state.json | jq '.plan_mode_completed'
cat .claude/completion-checkpoint.json | jq '.self_report.is_job_complete'
ls -la src/
Phase 5: Cleanup
rm -rf "$TEST_DIR"
python3 ~/.claude/hooks/worktree-manager.py cleanup "test-xxx"
Test Case Patterns
Pattern 1: Hook Behavior Verification
Test that hooks fire correctly based on state files.
#!/bin/bash
TEST_DIR=$(mktemp -d)
cd "$TEST_DIR"
git init && git commit --allow-empty -m "init"
mkdir -p .claude
echo '{"iteration": 1, "plan_mode_completed": false}' > .claude/appfix-state.json
claude -p "Write 'test' to src/test.py" \
--dangerously-skip-permissions \
--no-session-persistence \
--output-format json > output.json 2>&1
if [[ -f src/test.py ]]; then
echo "FAIL: Code write was not blocked"
else
echo "PASS: Plan mode enforcer blocked code write"
fi
rm -rf "$TEST_DIR"
Pattern 2: Full Skill Lifecycle
Test a skill from start to completion.
#!/bin/bash
TEST_DIR=$(mktemp -d)
cd "$TEST_DIR"
git init && git commit --allow-empty -m "init"
mkdir src
echo "print('hello')" > src/main.py
claude -p "/melt: Add a greeting function to main.py" \
--dangerously-skip-permissions \
--no-session-persistence \
--output-format json \
--max-turns 15 > output.json 2>&1
if [[ -f .claude/completion-checkpoint.json ]]; then
IS_COMPLETE=$(jq '.self_report.is_job_complete' .claude/completion-checkpoint.json)
echo "Job complete: $IS_COMPLETE"
fi
rm -rf "$TEST_DIR"
Pattern 3: Parallel Agent Isolation
Test multiple agents working on the same codebase.
#!/bin/bash
MAIN_REPO="/path/to/your/repo"
cd "$MAIN_REPO"
AGENT1_PATH=$(python3 ~/.claude/hooks/worktree-manager.py create "agent-1" | tail -1)
AGENT2_PATH=$(python3 ~/.claude/hooks/worktree-manager.py create "agent-2" | tail -1)
(cd "$AGENT1_PATH" && claude -p "Fix auth module" --dangerously-skip-permissions --no-session-persistence) &
(cd "$AGENT2_PATH" && claude -p "Fix database module" --dangerously-skip-permissions --no-session-persistence) &
wait
python3 ~/.claude/hooks/worktree-manager.py merge "agent-1"
python3 ~/.claude/hooks/worktree-manager.py merge "agent-2"
python3 ~/.claude/hooks/worktree-manager.py cleanup "agent-1"
python3 ~/.claude/hooks/worktree-manager.py cleanup "agent-2"
tmux Interactive Test Runner
For skills that require interactive observation:
#!/bin/bash
SKILL_NAME="${1:-appfix}"
TEST_PROMPT="${2:-/appfix}"
SESSION_NAME="skill-test-$SKILL_NAME-$(date +%s)"
TEST_DIR=$(mktemp -d "/tmp/skill-test-XXXXXX")
(cd "$TEST_DIR" && git init -q && git commit --allow-empty -m "init" -q)
tmux new-session -d -s "$SESSION_NAME" -x 200 -y 50
tmux send-keys -t "$SESSION_NAME" "cd '$TEST_DIR'" Enter
sleep 1
tmux send-keys -t "$SESSION_NAME" "claude --dangerously-skip-permissions" Enter
sleep 5
tmux send-keys -t "$SESSION_NAME" "$TEST_PROMPT" Enter
echo "Session started: $SESSION_NAME"
echo "Attach with: tmux attach -t $SESSION_NAME"
echo "Kill with: tmux kill-session -t $SESSION_NAME"
echo "Test dir: $TEST_DIR"
Assertions Library
Common verification patterns:
assert_file_exists() {
[[ -f "$1" ]] || { echo "FAIL: $1 not found"; return 1; }
echo "PASS: $1 exists"
}
assert_file_contains() {
grep -q "$2" "$1" || { echo "FAIL: '$2' not in $1"; return 1; }
echo "PASS: $1 contains '$2'"
}
assert_checkpoint_field() {
local field="$1"
local expected="$2"
local actual=$(jq -r ".self_report.$field" .claude/completion-checkpoint.json 2>/dev/null)
[[ "$actual" == "$expected" ]] || { echo "FAIL: $field=$actual (expected $expected)"; return 1; }
echo "PASS: $field=$expected"
}
() {
filename=
field=
expected=
actual=$(jq -r 2>/dev/null)
[[ == ]] || { ; 1; }
}
Integration with Existing Tests
The toolkit already has test infrastructure:
| Script | Mode | Purpose |
|---|
scripts/test-e2e-headless.sh | Headless | Automated hook tests |
scripts/test-e2e-tmux.sh | Interactive | Manual observation tests |
Run existing tests:
cd ~/Desktop/motium_github/documentation/prompts
bash scripts/test-e2e-headless.sh
bash scripts/test-e2e-tmux.sh --observe
Limitations
Cannot Test
- SessionStart hooks in child sessions - Hooks are captured when Claude starts, not when hooks change
- Real OAuth flows - Credentials must be pre-configured
- True parallel sessions - Rate limits apply across all sessions
- Long-running tasks in headless mode - Context overflow risk (#13831)
Must Restart Session For
- Changes to
~/.claude/settings.json
- Changes to hook scripts in
~/.claude/hooks/
- Changes to skill definitions in
~/.claude/skills/
Debug Tips
tail -f /tmp/claude-hooks-debug.log
python3 -c "
import json
from pathlib import Path
state = json.loads(Path('.claude/appfix-state.json').read_text())
print('started_at:', state.get('started_at'))
print('plan_mode_completed:', state.get('plan_mode_completed'))
"
cat ~/.claude/settings.json | jq '.hooks'
References
Sources
Platform capabilities verified through:
Security References
tmux Environment Isolation
Sandbox Escape Vectors (Why We Need These Mitigations)
- Container Escape Vulnerabilities (Sysdig) - general principles apply to process isolation
- macOS lacks Linux namespaces;
$HOME override is our primary isolation mechanism
- File-level isolation via worktrees; no chroot available without root
Credential Storage Findings
Analysis of ~/.claude/ directory revealed:
.credentials-export.json - OAuth tokens (sk-ant-oat01-*, sk-ant-ort01-*)
settings.json - Hook configuration (safe to share read-only)
{skill}-state.json - Skill activation state (race condition risk)
projects/ - Session logs (may contain sensitive data)
State File Race Conditions
From hooks/_common.py:
user_state_path = Path.home() / ".claude" / "appfix-state.json"
Multiple sessions writing to the same state file can:
- Corrupt JSON structure
- Overwrite session IDs (breaking sticky sessions)
- Reset iteration counters (causing infinite loops)
The sandbox mitigates this by overriding $HOME so Path.home() resolves to sandbox directory.
Production Deployment Risks
Skills like deploy-pipeline can execute:
gh workflow run cortex-backend-ci.yml -f environment=production
az containerapp update --name aca-motium-cortex-api-prod ...
The sandbox mock commands ($SANDBOX_DIR/bin/gh, $SANDBOX_DIR/bin/az) intercept and block these operations, logging them to blocked-commands.log.