Skip to content

Commit 4b30870

Browse files
authored
Merge pull request #2 from wwwoda/feat/skip-permissions-flag
Add --skip-permissions flag for Sail container sandboxed execution
2 parents 0d606e3 + 584e930 commit 4b30870

3 files changed

Lines changed: 64 additions & 10 deletions

File tree

scripts/ralph-loop.cjs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function parseArgs() {
4141
sessionId: null,
4242
budget: null,
4343
fresh: false,
44+
skipPermissions: false,
4445
logPath: null,
4546
maxConsecutiveFailures: parseInt(
4647
process.env.AGENT_MAX_CONSECUTIVE_FAILURES || "3",
@@ -78,6 +79,15 @@ function parseArgs() {
7879
case "--fresh":
7980
parsed.fresh = true;
8081
break;
82+
case "--skip-permissions":
83+
if (process.env.LARAVEL_SAIL !== "1") {
84+
console.error(
85+
`${color.red}Error: --skip-permissions requires LARAVEL_SAIL=1 (Sail container).${color.reset}`,
86+
);
87+
process.exit(1);
88+
}
89+
parsed.skipPermissions = true;
90+
break;
8191
case "--log-path":
8292
parsed.logPath = args[++i];
8393
break;
@@ -314,10 +324,14 @@ function buildClaudeArgs(config, prompt, iteration) {
314324
"--verbose",
315325
"--output-format",
316326
"stream-json",
317-
"--permission-mode",
318-
config.permissionMode,
319327
];
320328

329+
if (config.skipPermissions) {
330+
commonArgs.push("--dangerously-skip-permissions");
331+
} else {
332+
commonArgs.push("--permission-mode", config.permissionMode);
333+
}
334+
321335
if (config.model) {
322336
commonArgs.push("--model", config.model);
323337
}
@@ -383,6 +397,9 @@ async function main() {
383397
console.log(
384398
`${color.bold}${color.blue}║ Resume: ${resumeMode ? "enabled" : "disabled"}${color.reset}`,
385399
);
400+
console.log(
401+
`${color.bold}${color.blue}║ Skip permissions: ${config.skipPermissions ? "yes" : "no"}${color.reset}`,
402+
);
386403
console.log(
387404
`${color.bold}${color.blue}║ Log: ${logger.path}${color.reset}`,
388405
);
@@ -393,7 +410,7 @@ async function main() {
393410

394411
logger.info(`Starting ralph loop: ${config.name}`);
395412
logger.info(`Iterations: ${config.iterations}`);
396-
logger.info(`Permission mode: ${config.permissionMode}`);
413+
logger.info(`Permissions: ${config.skipPermissions ? "skipped (dangerously)" : `mode=${config.permissionMode}`}`);
397414
logger.info(`Model: ${config.model || "default"}`);
398415
logger.info(`Session ID: ${config.sessionId || "none"}`);
399416
logger.info(`Resume: ${resumeMode ? "enabled" : "disabled"}`);
@@ -447,7 +464,12 @@ async function main() {
447464
);
448465
logger.info("Retrying iteration as fresh invocation");
449466

450-
const freshArgs = ["-p", fullPrompt, "--verbose", "--output-format", "stream-json", "--permission-mode", config.permissionMode];
467+
const freshArgs = ["-p", fullPrompt, "--verbose", "--output-format", "stream-json"];
468+
if (config.skipPermissions) {
469+
freshArgs.push("--dangerously-skip-permissions");
470+
} else {
471+
freshArgs.push("--permission-mode", config.permissionMode);
472+
}
451473
if (config.model) freshArgs.push("--model", config.model);
452474
if (config.budget) freshArgs.push("--max-budget-usd", config.budget);
453475

src/Commands/StartCommand.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ class StartCommand extends Command
2828
{--fresh : Each iteration starts a fresh Claude session}
2929
{--resume : Resume a previously stopped session}
3030
{--attach : Attach to screen session after starting}
31-
{--once : Run single iteration in foreground}';
31+
{--once : Run single iteration in foreground}
32+
{--skip-permissions : Run Claude with --dangerously-skip-permissions (requires Sail container)}';
3233

3334
protected $description = 'Start a Ralph agent loop';
3435

@@ -40,6 +41,12 @@ public function handle(SessionTracker $tracker, ScreenManager $screenManager): i
4041
return self::FAILURE;
4142
}
4243

44+
if ($this->option('skip-permissions') && ! getenv('LARAVEL_SAIL')) {
45+
$this->components->error('The --skip-permissions flag requires a Sail container (LARAVEL_SAIL=1)');
46+
47+
return self::FAILURE;
48+
}
49+
4350
if (! $this->validateEnvironment()) {
4451
return self::FAILURE;
4552
}
@@ -72,6 +79,7 @@ public function handle(SessionTracker $tracker, ScreenManager $screenManager): i
7279
$logger->info("Iterations: {$iterations}");
7380
$logger->info('Model: '.($model ?? 'default'));
7481
$logger->info('Mode: '.($this->option('fresh') ? 'fresh' : 'resume'));
82+
$logger->info('Skip permissions: '.($this->option('skip-permissions') ? 'true' : 'false'));
7583
$logger->info("Working dir: {$workingDir}");
7684

7785
// Build the ralph-loop command
@@ -456,20 +464,24 @@ private function resolveModel(): ?string
456464

457465
private function buildLoopCommand(string $scriptPath, string $prompt, string $name, int $iterations, string $sessionId, string $logPath): string
458466
{
459-
/** @var string $permissionMode */
460-
$permissionMode = config('ralph.loop.permission_mode');
461-
462467
$cmd = sprintf(
463-
'node %s --prompt %s --name %s --iterations %d --permission-mode %s --session-id %s --log-path %s',
468+
'node %s --prompt %s --name %s --iterations %d --session-id %s --log-path %s',
464469
escapeshellarg($scriptPath),
465470
escapeshellarg($prompt),
466471
escapeshellarg($name),
467472
$iterations,
468-
escapeshellarg($permissionMode),
469473
escapeshellarg($sessionId),
470474
escapeshellarg($logPath),
471475
);
472476

477+
if ($this->option('skip-permissions')) {
478+
$cmd .= ' --skip-permissions';
479+
} else {
480+
/** @var string $permissionMode */
481+
$permissionMode = config('ralph.loop.permission_mode');
482+
$cmd .= ' --permission-mode '.escapeshellarg($permissionMode);
483+
}
484+
473485
$model = $this->resolveModel();
474486
if (is_string($model)) {
475487
$cmd .= ' --model '.escapeshellarg($model);

tests/Feature/RalphCommandsTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,26 @@
105105
File::deleteDirectory($claudeDir);
106106
});
107107

108+
test('ralph:start --skip-permissions fails without LARAVEL_SAIL', function () {
109+
// Ensure LARAVEL_SAIL is not set
110+
putenv('LARAVEL_SAIL');
111+
unset($_ENV['LARAVEL_SAIL'], $_SERVER['LARAVEL_SAIL']);
112+
113+
$this->artisan('ralph:start --skip-permissions --once --prompt "test" test-session')
114+
->expectsOutputToContain('--skip-permissions flag requires a Sail container')
115+
->assertExitCode(1);
116+
});
117+
118+
test('ralph:start --skip-permissions guard checks LARAVEL_SAIL env', function () {
119+
// Verify the guard logic directly: with LARAVEL_SAIL=1, getenv() returns truthy
120+
putenv('LARAVEL_SAIL=1');
121+
expect(getenv('LARAVEL_SAIL'))->toBeTruthy();
122+
123+
// Without LARAVEL_SAIL, getenv() returns false (falsy)
124+
putenv('LARAVEL_SAIL');
125+
expect(getenv('LARAVEL_SAIL'))->toBeFalsy();
126+
});
127+
108128
test('ralph:init fails on invalid existing json', function () {
109129
$claudeDir = base_path('.claude');
110130
$settingsPath = $claudeDir.'/settings.json';

0 commit comments

Comments
 (0)