Skip to content

Commit 1ddbb2f

Browse files
authored
feature/ Implement docker sandbox (#1)
* feat: implement docker sandbox * chore: increment versions to 1.0.4 and 0.1.4
1 parent bceb996 commit 1ddbb2f

12 files changed

Lines changed: 592 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,18 @@ export const DEFAULT_CONFIG = {
173173
showSplash: true,
174174
enableNotifications: true,
175175
enableSound: true,
176+
sandbox: false,
176177
};
177178
```
178179

180+
### Docker Sandbox Mode
181+
182+
Run Claude inside a Docker container for isolated execution:
183+
- Requires Docker Desktop 4.50+ with sandbox plugin
184+
- Enable with `--sandbox` flag
185+
- Uses `docker sandbox run --credentials host` to pass credentials
186+
- Related files: `src/lib/docker.ts`, `src/lib/prompt.ts`
187+
179188
## History
180189

181190
Runs are saved to `~/.ralph/history/{id}.json` with full iteration records.

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ ralph -m 50 "Implement feature X"
6767
| `--no-sound` | Disable sound alerts | - |
6868
| `--debug` | Enable debug logging | false |
6969
| `--preflight-only` | Only run pre-flight checks | - |
70+
| `--sandbox` | Run Claude in Docker sandbox | false |
71+
| `--no-sandbox` | Disable Docker sandbox (default) | - |
7072

7173
### Keyboard Controls
7274

@@ -165,6 +167,43 @@ Ralph runs several checks before starting:
165167

166168
Run `ralph --preflight-only` to test without starting the loop.
167169

170+
## Docker Sandbox Mode
171+
172+
For enhanced security during AFK or overnight runs, Ralph supports running Claude inside a Docker sandbox container.
173+
174+
### Prerequisites
175+
176+
- Docker Desktop 4.50+ installed and running
177+
- Docker sandbox plugin enabled
178+
179+
### Usage
180+
181+
```bash
182+
# Run with Docker sandbox
183+
ralph --sandbox "Your prompt here"
184+
185+
# Combine with other options
186+
ralph --sandbox -m 100 -M sonnet ./my-prd.md
187+
```
188+
189+
### How It Works
190+
191+
When sandbox mode is enabled, Ralph runs Claude via Docker's sandbox feature:
192+
```bash
193+
docker sandbox run --credentials host claude [args]
194+
```
195+
196+
This provides:
197+
- **Isolated execution**: Claude runs in a container, not directly on your host
198+
- **Controlled access**: Uses `--credentials host` to pass through your Claude credentials
199+
- **Same functionality**: All Claude features work normally inside the sandbox
200+
201+
### Fallback Behavior
202+
203+
If Docker sandbox is not available (Docker not installed, not running, or missing the sandbox plugin), Ralph will prompt you to:
204+
- Continue without sandbox (runs Claude directly)
205+
- Exit and fix Docker setup
206+
168207
## Development
169208

170209
```bash

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@yazangineering/ralph",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Autonomous AI coding loop CLI using Claude Code - named after Ralph Wiggum's naive, relentless persistence",
55
"type": "module",
66
"main": "dist/cli.js",

src/cli.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import { App } from './app.js';
1818
import { runPreflightChecks, formatPreflightResults } from './lib/preflight.js';
1919
import { logger, setDebugEnabled } from './lib/logger.js';
2020
import { setNotificationsEnabled, setSoundEnabled } from './lib/notifications.js';
21+
import { runDockerPreflightChecks } from './lib/docker.js';
22+
import { promptSandboxFallback } from './lib/prompt.js';
2123

2224
// Package version (will be replaced by build)
23-
const VERSION = '1.0.3';
25+
const VERSION = '1.0.4';
2426

2527
// CLI program
2628
const program = new Command();
@@ -40,6 +42,8 @@ program
4042
.option('--no-sound', 'Disable sound alerts')
4143
.option('--debug', 'Enable debug logging')
4244
.option('--preflight-only', 'Only run pre-flight checks')
45+
.option('--sandbox', 'Run Claude in Docker sandbox for isolation')
46+
.option('--no-sandbox', 'Disable Docker sandbox (default)')
4347
.action(async (promptArg: string | undefined, options) => {
4448
// Enable debug if requested
4549
if (options.debug) {
@@ -85,8 +89,25 @@ program
8589
prompt,
8690
isFile,
8791
projectRoot: process.cwd(),
92+
sandbox: options.sandbox || false,
8893
};
8994

95+
// Run Docker sandbox preflight checks if sandbox mode is enabled
96+
if (config.sandbox) {
97+
const dockerResult = await runDockerPreflightChecks();
98+
if (!dockerResult.passed) {
99+
const choice = await promptSandboxFallback(dockerResult);
100+
if (choice === 'exit') {
101+
process.exit(1);
102+
}
103+
// Continue without sandbox
104+
config.sandbox = false;
105+
logger.info('Continuing without Docker sandbox');
106+
} else {
107+
logger.info('Docker sandbox mode enabled');
108+
}
109+
}
110+
90111
// Run pre-flight checks
91112
const preflightResult = await runPreflightChecks(config);
92113

src/hooks/useClaudeLoop.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export function useClaudeLoop(options: UseClaudeLoopOptions): UseClaudeLoopRetur
8888
model: config.model,
8989
dangerouslySkipPermissions: config.dangerouslySkipPermissions,
9090
projectRoot: config.projectRoot,
91+
sandbox: config.sandbox,
9192
onOutput: (chunk) => {
9293
logger.debug(`[STREAM] Received chunk: ${chunk.length} chars`);
9394
setState((prev) => ({ ...prev, output: [...prev.output, chunk] }));

src/lib/claude.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { spawn, type ChildProcess } from 'node:child_process';
6-
import type { ClaudeProcessOptions, ClaudeProcessResult } from '../types/index.js';
6+
import type { ClaudeProcessOptions, ClaudeProcessResult, SpawnConfig } from '../types/index.js';
77
import { parsePromiseTag } from './promiseParser.js';
88
import { logger } from './logger.js';
99

@@ -33,18 +33,34 @@ export function buildClaudeArgs(options: ClaudeProcessOptions): string[] {
3333
return args;
3434
}
3535

36+
export function buildSpawnConfig(options: ClaudeProcessOptions): SpawnConfig {
37+
const claudeArgs = buildClaudeArgs(options);
38+
39+
if (options.sandbox) {
40+
return {
41+
command: 'docker',
42+
args: ['sandbox', 'run', '--credentials', 'host', 'claude', ...claudeArgs],
43+
};
44+
}
45+
46+
return {
47+
command: 'claude',
48+
args: claudeArgs,
49+
};
50+
}
51+
3652
export async function runClaude(options: ClaudeProcessOptions): Promise<ClaudeProcessResult> {
3753
const startTime = Date.now();
38-
const args = buildClaudeArgs(options);
54+
const spawnConfig = buildSpawnConfig(options);
3955

40-
logger.debug('Running Claude CLI', { args: ['claude', ...args].join(' ') });
56+
logger.debug('Running Claude CLI', { args: [spawnConfig.command, ...spawnConfig.args].join(' ') });
4157

4258
return new Promise((resolve) => {
4359
let stdout = '';
4460
let stderr = '';
4561
let hasError = false;
4662

47-
const proc = spawn('claude', args, {
63+
const proc = spawn(spawnConfig.command, spawnConfig.args, {
4864
cwd: options.projectRoot,
4965
env: { ...process.env, TERM: 'dumb' },
5066
stdio: ['ignore', 'pipe', 'pipe'],
@@ -222,6 +238,7 @@ export const claude = {
222238
runClaudeWithTimeout,
223239
killActiveProcess,
224240
buildClaudeArgs,
241+
buildSpawnConfig,
225242
isClaudeAvailable,
226243
getClaudeVersion,
227244
createMockClaudeRunner,

0 commit comments

Comments
 (0)