Skip to content

Commit e64d010

Browse files
committed
fix: stabilize Claude permission mode for unattended loops
- default Claude Code loops to permission-mode auto\n- validate and normalize Claude permission mode config\n- preserve managed .ralphrc upgrades and env precedence
1 parent 8b1d967 commit e64d010

9 files changed

Lines changed: 256 additions & 7 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ The instructions file and command directory depend on the configured platform. S
394394

395395
Ralph is a bash loop that spawns fresh AI coding sessions using a **platform driver** matching the configured platform:
396396

397-
- **Claude Code driver** — invokes `claude` with `--output-format json`, `--allowedTools`, and explicit `--resume <session_id>`
397+
- **Claude Code driver** — invokes `claude` with `--output-format json`, `--permission-mode auto`, `--allowedTools`, and explicit `--resume <session_id>`
398398
- **Codex driver** — invokes `codex exec --json --sandbox workspace-write` with explicit `--resume <session_id>`
399399
- **Copilot driver** _(experimental)_ — invokes `copilot --autopilot --yolo` with plain-text output
400400
- **Cursor driver** _(experimental)_ — invokes `cursor-agent -p --force --output-format json`, persists `session_id` for `--resume`, and switches to `stream-json` only for live output
@@ -451,6 +451,9 @@ If you get permission errors:
451451
# .ralph/.ralphrc
452452
ALLOWED_TOOLS="Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,NotebookEdit,Bash"
453453

454+
# Keep interactive approval workflows out of unattended Claude loops
455+
CLAUDE_PERMISSION_MODE="auto"
456+
454457
# Keep the loop unattended by continuing after detected denials
455458
PERMISSION_DENIAL_MODE="continue"
456459

@@ -461,7 +464,8 @@ bmalph run
461464

462465
Notes:
463466

464-
- `ALLOWED_TOOLS` only applies to the Claude Code driver.
467+
- `ALLOWED_TOOLS` only applies to the Claude Code driver and controls normal tool access.
468+
- `CLAUDE_PERMISSION_MODE="auto"` prevents interactive approval modes like plan approval from blocking unattended Claude loops.
465469
- Codex, Cursor, and Copilot use their native sandbox/approval settings instead.
466470
- Fresh installs default to unattended mode and discourage in-loop user questions via `.ralph/PROMPT.md`.
467471

ralph/RALPH-REFERENCE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Environment variables > Ralph config file > script defaults
6161
| `CLAUDE_TIMEOUT_MINUTES` | `15` | Timeout per loop driver invocation |
6262
| `CLAUDE_OUTPUT_FORMAT` | `json` | Output format (json or text) |
6363
| `ALLOWED_TOOLS` | `Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,NotebookEdit,Bash` | Claude Code only. Ignored by codex, cursor, and copilot |
64+
| `CLAUDE_PERMISSION_MODE` | `auto` | Claude Code only. Prevents interactive approval workflows from blocking unattended loops |
6465
| `PERMISSION_DENIAL_MODE` | `continue` | How Ralph responds to permission denials: continue, halt, or threshold |
6566
| `SESSION_CONTINUITY` | `true` | Maintain context across loops |
6667
| `SESSION_EXPIRY_HOURS` | `24` | Session expiration time |
@@ -340,9 +341,10 @@ When using `--monitor` with `--live`, tmux creates a 3-pane layout:
340341

341342
**Solutions:**
342343
1. For Claude Code, update `ALLOWED_TOOLS` in `.ralph/.ralphrc` to include needed tools
343-
2. For codex, cursor, and copilot, review the driver's native permission settings; `ALLOWED_TOOLS` is ignored
344-
3. If you want unattended behavior, keep `PERMISSION_DENIAL_MODE="continue"` in `.ralph/.ralphrc`
345-
4. Reset circuit breaker if needed: `bash .ralph/ralph_loop.sh --reset-circuit`
344+
2. For Claude Code unattended loops, keep `CLAUDE_PERMISSION_MODE="auto"` in `.ralph/.ralphrc`
345+
3. For codex, cursor, and copilot, review the driver's native permission settings; `ALLOWED_TOOLS` is ignored
346+
4. If you want unattended behavior, keep `PERMISSION_DENIAL_MODE="continue"` in `.ralph/.ralphrc`
347+
5. Reset circuit breaker if needed: `bash .ralph/ralph_loop.sh --reset-circuit`
346348

347349
#### Session expires mid-project
348350

ralph/drivers/claude-code.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ driver_supports_tool_allowlist() {
5454
}
5555

5656
driver_permission_denial_help() {
57-
echo " 1. Edit $RALPHRC_FILE and update ALLOWED_TOOLS to include the required tools"
58-
echo " 2. Common patterns:"
57+
echo " 1. Edit $RALPHRC_FILE and keep CLAUDE_PERMISSION_MODE=auto for unattended Claude Code loops"
58+
echo " 2. If Claude was denied on an interactive approval step, ALLOWED_TOOLS will not fix it"
59+
echo " 3. If Claude was denied on a normal tool, update ALLOWED_TOOLS to include the required tools"
60+
echo " 4. Common ALLOWED_TOOLS patterns:"
5961
echo " - Bash - All shell commands"
6062
echo " - Bash(node *) - All Node.js commands"
6163
echo " - Bash(npm *) - All npm commands"
@@ -77,6 +79,7 @@ driver_build_command() {
7779
local prompt_file=$1
7880
local loop_context=$2
7981
local session_id=$3
82+
local resolved_permission_mode="${CLAUDE_PERMISSION_MODE:-auto}"
8083

8184
# Note: We do NOT use --dangerously-skip-permissions here. Tool permissions
8285
# are controlled via --allowedTools from CLAUDE_ALLOWED_TOOLS in .ralphrc.
@@ -93,6 +96,9 @@ driver_build_command() {
9396
CLAUDE_CMD_ARGS+=("--output-format" "json")
9497
fi
9598

99+
# Prevent interactive approval flows from blocking unattended -p loops.
100+
CLAUDE_CMD_ARGS+=("--permission-mode" "$resolved_permission_mode")
101+
96102
# Allowed tools
97103
if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
98104
CLAUDE_CMD_ARGS+=("--allowedTools")

ralph/ralph_loop.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ _env_MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-}"
4040
_env_CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-}"
4141
_env_CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-}"
4242
_env_CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
43+
_env_has_CLAUDE_PERMISSION_MODE="${CLAUDE_PERMISSION_MODE+x}"
44+
_env_CLAUDE_PERMISSION_MODE="${CLAUDE_PERMISSION_MODE:-}"
4345
_env_CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-}"
4446
_env_CLAUDE_SESSION_EXPIRY_HOURS="${CLAUDE_SESSION_EXPIRY_HOURS:-}"
4547
_env_ALLOWED_TOOLS="${ALLOWED_TOOLS:-}"
@@ -77,6 +79,7 @@ DEFAULT_PERMISSION_DENIAL_MODE="continue"
7779
# Modern Claude CLI configuration (Phase 1.1)
7880
CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-json}"
7981
CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-$DEFAULT_CLAUDE_ALLOWED_TOOLS}"
82+
CLAUDE_PERMISSION_MODE="${CLAUDE_PERMISSION_MODE:-auto}"
8083
CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-true}"
8184
PERMISSION_DENIAL_MODE="${PERMISSION_DENIAL_MODE:-$DEFAULT_PERMISSION_DENIAL_MODE}"
8285
CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
@@ -158,6 +161,7 @@ resolve_ralphrc_file() {
158161
# - MAX_CALLS_PER_HOUR
159162
# - CLAUDE_TIMEOUT_MINUTES
160163
# - CLAUDE_OUTPUT_FORMAT
164+
# - CLAUDE_PERMISSION_MODE
161165
# - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS for Claude Code only)
162166
# - PERMISSION_DENIAL_MODE
163167
# - SESSION_CONTINUITY (mapped to CLAUDE_USE_CONTINUE)
@@ -203,6 +207,9 @@ load_ralphrc() {
203207
[[ -n "$_env_CLAUDE_TIMEOUT_MINUTES" ]] && CLAUDE_TIMEOUT_MINUTES="$_env_CLAUDE_TIMEOUT_MINUTES"
204208
[[ -n "$_env_CLAUDE_OUTPUT_FORMAT" ]] && CLAUDE_OUTPUT_FORMAT="$_env_CLAUDE_OUTPUT_FORMAT"
205209
[[ -n "$_env_CLAUDE_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_CLAUDE_ALLOWED_TOOLS"
210+
if [[ "$_env_has_CLAUDE_PERMISSION_MODE" == "x" ]]; then
211+
CLAUDE_PERMISSION_MODE="$_env_CLAUDE_PERMISSION_MODE"
212+
fi
206213
[[ -n "$_env_CLAUDE_USE_CONTINUE" ]] && CLAUDE_USE_CONTINUE="$_env_CLAUDE_USE_CONTINUE"
207214
[[ -n "$_env_CLAUDE_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_CLAUDE_SESSION_EXPIRY_HOURS"
208215
[[ -n "$_env_PERMISSION_DENIAL_MODE" ]] && PERMISSION_DENIAL_MODE="$_env_PERMISSION_DENIAL_MODE"
@@ -229,6 +236,7 @@ load_ralphrc() {
229236
[[ -n "$_env_CB_COOLDOWN_MINUTES" ]] && CB_COOLDOWN_MINUTES="$_env_CB_COOLDOWN_MINUTES"
230237
[[ -n "$_env_CB_AUTO_RESET" ]] && CB_AUTO_RESET="$_env_CB_AUTO_RESET"
231238

239+
normalize_claude_permission_mode
232240
RALPHRC_FILE="$config_file"
233241
RALPHRC_LOADED=true
234242
return 0
@@ -241,6 +249,7 @@ driver_supports_tool_allowlist() {
241249
driver_permission_denial_help() {
242250
echo " - Review the active driver's permission or approval settings."
243251
echo " - ALLOWED_TOOLS in $RALPHRC_FILE only applies to the Claude Code driver."
252+
echo " - Keep CLAUDE_PERMISSION_MODE=auto for unattended Claude Code loops."
244253
echo " - After updating permissions, reset the session and restart the loop."
245254
}
246255

@@ -519,6 +528,27 @@ validate_permission_denial_mode() {
519528
esac
520529
}
521530

531+
normalize_claude_permission_mode() {
532+
if [[ -z "${CLAUDE_PERMISSION_MODE:-}" ]]; then
533+
CLAUDE_PERMISSION_MODE="auto"
534+
fi
535+
}
536+
537+
validate_claude_permission_mode() {
538+
local mode=$1
539+
540+
case "$mode" in
541+
auto|acceptEdits|bypassPermissions|default|dontAsk|plan)
542+
return 0
543+
;;
544+
*)
545+
echo "Error: Invalid CLAUDE_PERMISSION_MODE: '$mode'"
546+
echo "Valid modes: auto acceptEdits bypassPermissions default dontAsk plan"
547+
return 1
548+
;;
549+
esac
550+
}
551+
522552
warn_if_allowed_tools_ignored() {
523553
if driver_supports_tool_allowlist; then
524554
return 0
@@ -1633,6 +1663,14 @@ main() {
16331663
exit 1
16341664
fi
16351665

1666+
if [[ "$(driver_name)" == "claude-code" ]]; then
1667+
normalize_claude_permission_mode
1668+
1669+
if ! validate_claude_permission_mode "$CLAUDE_PERMISSION_MODE"; then
1670+
exit 1
1671+
fi
1672+
fi
1673+
16361674
if driver_supports_tool_allowlist; then
16371675
# Validate --allowed-tools now that platform-specific VALID_TOOL_PATTERNS are loaded
16381676
if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" ]] && ! validate_allowed_tools "$CLAUDE_ALLOWED_TOOLS"; then

ralph/templates/ralphrc.template

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ CLAUDE_OUTPUT_FORMAT="json"
4545
# Opt in to interactive pauses by adding AskUserQuestion manually.
4646
ALLOWED_TOOLS="Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,NotebookEdit,Bash"
4747

48+
# Permission mode for Claude Code CLI (default: auto)
49+
# Options: auto, acceptEdits, bypassPermissions, default, dontAsk, plan
50+
CLAUDE_PERMISSION_MODE="auto"
51+
4852
# How Ralph responds when a driver reports permission denials:
4953
# - continue: log the denial and keep looping (default for unattended mode)
5054
# - halt: stop immediately and show recovery guidance

src/installer/template-files.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const TEMPLATE_PLACEHOLDERS: Record<string, string> = {
99
};
1010

1111
const RALPHRC_TEMPLATE_NAME = "RALPHRC";
12+
const CLAUDE_PERMISSION_MODE_TEMPLATE_BLOCK = `# Permission mode for Claude Code CLI (default: auto)
13+
# Options: auto, acceptEdits, bypassPermissions, default, dontAsk, plan
14+
CLAUDE_PERMISSION_MODE="auto"
15+
16+
`;
1217

1318
const LEGACY_RALPHRC_TEMPLATE = `# .ralphrc - Ralph project configuration
1419
# Generated by: ralph enable
@@ -145,6 +150,14 @@ async function isRalphrcCustomized(filePath: string, platformId: string): Promis
145150
return false;
146151
}
147152

153+
const previousManagedTemplate = currentTemplate.replace(
154+
CLAUDE_PERMISSION_MODE_TEMPLATE_BLOCK,
155+
""
156+
);
157+
if (content === previousManagedTemplate) {
158+
return false;
159+
}
160+
148161
const legacyTemplate = renderLegacyRalphrcTemplate(platformId);
149162
return content !== legacyTemplate;
150163
}

tests/bash/drivers/claude_code.bats

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ teardown() {
6666
[[ "$found_git" == "true" ]]
6767
}
6868

69+
@test "driver_permission_denial_help distinguishes permission mode from allowed tools" {
70+
RALPHRC_FILE="$RALPH_DIR/.ralphrc"
71+
72+
run driver_permission_denial_help
73+
74+
assert_success
75+
assert_output --partial "CLAUDE_PERMISSION_MODE=auto"
76+
assert_output --partial "ALLOWED_TOOLS"
77+
assert_output --partial "will not fix it"
78+
assert_output --partial "--reset-session"
79+
assert_output --partial "bmalph run"
80+
}
81+
6982
# ===========================================================================
7083
# driver_build_command
7184
# ===========================================================================
@@ -93,6 +106,7 @@ teardown() {
93106

94107
export CLAUDE_OUTPUT_FORMAT="json"
95108
export CLAUDE_ALLOWED_TOOLS=""
109+
export CLAUDE_PERMISSION_MODE=""
96110
export CLAUDE_USE_CONTINUE="false"
97111

98112
driver_build_command "$prompt_file" "" ""
@@ -101,12 +115,43 @@ teardown() {
101115
[[ "$args_str" =~ "--output-format json" ]]
102116
}
103117

118+
@test "driver_build_command defaults permission mode to auto" {
119+
local prompt_file="$RALPH_DIR/prompt.md"
120+
echo "Test prompt" > "$prompt_file"
121+
122+
export CLAUDE_OUTPUT_FORMAT=""
123+
export CLAUDE_ALLOWED_TOOLS=""
124+
export CLAUDE_PERMISSION_MODE=""
125+
export CLAUDE_USE_CONTINUE="false"
126+
127+
driver_build_command "$prompt_file" "" ""
128+
129+
local args_str="${CLAUDE_CMD_ARGS[*]}"
130+
[[ "$args_str" =~ "--permission-mode auto" ]]
131+
}
132+
133+
@test "driver_build_command passes configured permission mode" {
134+
local prompt_file="$RALPH_DIR/prompt.md"
135+
echo "Test prompt" > "$prompt_file"
136+
137+
export CLAUDE_OUTPUT_FORMAT=""
138+
export CLAUDE_ALLOWED_TOOLS=""
139+
export CLAUDE_PERMISSION_MODE="dontAsk"
140+
export CLAUDE_USE_CONTINUE="false"
141+
142+
driver_build_command "$prompt_file" "" ""
143+
144+
local args_str="${CLAUDE_CMD_ARGS[*]}"
145+
[[ "$args_str" =~ "--permission-mode dontAsk" ]]
146+
}
147+
104148
@test "driver_build_command adds allowed tools" {
105149
local prompt_file="$RALPH_DIR/prompt.md"
106150
echo "Test prompt" > "$prompt_file"
107151

108152
export CLAUDE_OUTPUT_FORMAT=""
109153
export CLAUDE_ALLOWED_TOOLS="Write,Read,Bash"
154+
export CLAUDE_PERMISSION_MODE=""
110155
export CLAUDE_USE_CONTINUE="false"
111156

112157
driver_build_command "$prompt_file" "" ""
@@ -124,6 +169,7 @@ teardown() {
124169

125170
export CLAUDE_OUTPUT_FORMAT=""
126171
export CLAUDE_ALLOWED_TOOLS=""
172+
export CLAUDE_PERMISSION_MODE=""
127173
export CLAUDE_USE_CONTINUE="true"
128174

129175
driver_build_command "$prompt_file" "" "session-abc-123"
@@ -138,6 +184,7 @@ teardown() {
138184

139185
export CLAUDE_OUTPUT_FORMAT=""
140186
export CLAUDE_ALLOWED_TOOLS=""
187+
export CLAUDE_PERMISSION_MODE=""
141188
export CLAUDE_USE_CONTINUE="true"
142189

143190
driver_build_command "$prompt_file" "" ""
@@ -152,6 +199,7 @@ teardown() {
152199

153200
export CLAUDE_OUTPUT_FORMAT=""
154201
export CLAUDE_ALLOWED_TOOLS=""
202+
export CLAUDE_PERMISSION_MODE=""
155203
export CLAUDE_USE_CONTINUE="false"
156204

157205
driver_build_command "$prompt_file" "" "session-abc-123"
@@ -166,6 +214,7 @@ teardown() {
166214

167215
export CLAUDE_OUTPUT_FORMAT=""
168216
export CLAUDE_ALLOWED_TOOLS=""
217+
export CLAUDE_PERMISSION_MODE=""
169218
export CLAUDE_USE_CONTINUE="false"
170219

171220
driver_build_command "$prompt_file" "Loop 3 context: 2 files changed" ""

0 commit comments

Comments
 (0)