fix: avoid Claude chat exit when bypass mode runs as root#702
fix: avoid Claude chat exit when bypass mode runs as root#702michaeledi wants to merge 1 commit intositeboon:mainfrom
Conversation
📝 WalkthroughWalkthroughAddresses a Claude SDK issue where running as root caused chat requests to exit with code 1. Implements a permission-mode resolver that maps both Changes
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
package.json (1)
46-46: LGTM on the new script.Rebuilding the server before running the test is the right call since the test imports from
dist-server/. Minor nit: consider adding a top-leveltest(ortest:all) script so this suite is discoverable and runnable alongside future tests via a single entry point.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 46, Add a top-level test entry point so the new suite is discoverable and runnable from one command: update package.json to include a "test" (or "test:all") script that aggregates existing test scripts and includes "test:claude-sdk-permissions" (reference the existing script name "test:claude-sdk-permissions") so running npm run test will build the server and execute this suite along with other tests.features.md (1)
1-5: Optional: duplicated with README/CHANGELOG.These two bullets restate what's already in
README.md("Root-Run Claude Chat" section) andCHANGELOG.md. Iffeatures.mdis intended as a release-notes input, consider either documenting its purpose in the repo (CONTRIBUTING or release tooling config) or consolidating — otherwise the three sources are likely to drift.CHANGELOG.md (1)
16-16: Minor: consider linking the fix to an issue/commit.Other entries in this section include a commit hash link (e.g.,
([3969135](…))). This new bullet lacks it — presumably it'll be auto-populated by the release tooling, but if not, add the commit/PR link so readers can trace the change from the changelog.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CHANGELOG.md` at line 16, The CHANGELOG entry about Claude chat mapping bypass permissions should include a link to the authoritative commit or PR for traceability; update the bullet "* Claude chat now maps both skip-permissions and explicit `bypassPermissions` chat mode to `dontAsk` when CloudCLI runs as root, avoiding Claude SDK exit code 1 from forbidden bypass mode" by appending the commit or PR reference (e.g., "([commit-hash](...))" or "([`#PR-number`](...))") matching the style used elsewhere in the section so readers can trace the change.server/modules/providers/tests/claude-sdk-permission-mode.test.mjs (1)
1-30: Good coverage; a few extra cases would harden the matrix.Current tests cover the happy-path matrix. Consider adding:
default+skipPermissions:false→ expectundefined(the function's "no override" path at Line 60 ofclaude-sdk.js).plan+skipPermissions:false→ expect'plan'(non-default passthrough).- Non‑root + plan +
skipPermissions:true→ expect'plan'(mirror of the existing root-only plan test).- A case relying on auto-detection (omit
optionsentirely) to exercise theprocess.getuidfallback.➕ Suggested additions
+test('returns undefined when permissionMode is default and skipPermissions is disabled', () => { + assert.equal(resolveSdkPermissionMode('default', { skipPermissions: false }, { isRoot: true }), undefined); +}); + +test('preserves plan mode when skipPermissions is disabled', () => { + assert.equal(resolveSdkPermissionMode('plan', { skipPermissions: false }, { isRoot: false }), 'plan'); +}); + +test('preserves plan mode for non-root sessions with skipPermissions enabled', () => { + assert.equal(resolveSdkPermissionMode('plan', { skipPermissions: true }, { isRoot: false }), 'plan'); +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/modules/providers/tests/claude-sdk-permission-mode.test.mjs` around lines 1 - 30, Add four tests to claude-sdk-permission-mode.test.mjs exercising the untested branches of resolveSdkPermissionMode: (1) assert that resolveSdkPermissionMode('default', { skipPermissions: false }, { isRoot: true }) returns undefined to cover the "no override" path, (2) assert that resolveSdkPermissionMode('plan', { skipPermissions: false }, { isRoot: true }) returns 'plan', (3) assert that resolveSdkPermissionMode('plan', { skipPermissions: true }, { isRoot: false }) returns 'plan' to mirror the existing root-only plan test for non-root sessions, and (4) add a test that omits the options argument (call resolveSdkPermissionMode('default') or similar) to exercise the process.getuid fallback/auto-detection behavior; ensure each test uses the same assert.equal pattern and references resolveSdkPermissionMode.README.md (1)
254-258: Clarify the security trade-off of mappingbypassPermissionstodontAskfor root execution.The README explains that root execution converts
bypassPermissionstodontAsk, but the behavioral implications aren't clear. Per the Claude Agent SDK docs, these modes have opposite approval semantics:bypassPermissionsauto-approves all tools, whiledontAskdenies any tool not explicitly pre-approved viaallowedTools/settings/hooks. Readers may misinterpret the mapping as equivalent behavior when it actually shifts from permissive to restrictive. Consider adding a sentence explaining that root mode enforces an explicit allow-list rather than allowing everything, so users understand the security boundary being enforced.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 254 - 258, Update the "Root-Run Claude Chat" section to explicitly state the security trade-off: clarify that mapping the chat mode's bypassPermissions to Claude's dontAsk under root does NOT preserve permissive behavior but instead enforces a restrictive allow-list model; mention that dontAsk denies any tool not explicitly approved via allowedTools or hooks while bypassPermissions would auto-approve tools, and include a short sentence pointing readers to configure allowedTools/hooks if they need specific tools to run as root; keep the existing test instruction (npm run test:claude-sdk-permissions) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/modules/providers/tests/claude-sdk-permission-mode.test.mjs`:
- Around line 3-6: Add the missing CI step to run the compiled-server test by
invoking the npm script "npm run test:claude-sdk-permissions" in the release
workflow (release.yml) before the publish step; update the workflow to run that
script (it depends on build:server already) so the test file
server/modules/providers/tests/claude-sdk-permission-mode.test.mjs is executed
during CI prior to publishing.
---
Nitpick comments:
In `@CHANGELOG.md`:
- Line 16: The CHANGELOG entry about Claude chat mapping bypass permissions
should include a link to the authoritative commit or PR for traceability; update
the bullet "* Claude chat now maps both skip-permissions and explicit
`bypassPermissions` chat mode to `dontAsk` when CloudCLI runs as root, avoiding
Claude SDK exit code 1 from forbidden bypass mode" by appending the commit or PR
reference (e.g., "([commit-hash](...))" or "([`#PR-number`](...))") matching the
style used elsewhere in the section so readers can trace the change.
In `@package.json`:
- Line 46: Add a top-level test entry point so the new suite is discoverable and
runnable from one command: update package.json to include a "test" (or
"test:all") script that aggregates existing test scripts and includes
"test:claude-sdk-permissions" (reference the existing script name
"test:claude-sdk-permissions") so running npm run test will build the server and
execute this suite along with other tests.
In `@README.md`:
- Around line 254-258: Update the "Root-Run Claude Chat" section to explicitly
state the security trade-off: clarify that mapping the chat mode's
bypassPermissions to Claude's dontAsk under root does NOT preserve permissive
behavior but instead enforces a restrictive allow-list model; mention that
dontAsk denies any tool not explicitly approved via allowedTools or hooks while
bypassPermissions would auto-approve tools, and include a short sentence
pointing readers to configure allowedTools/hooks if they need specific tools to
run as root; keep the existing test instruction (npm run
test:claude-sdk-permissions) unchanged.
In `@server/modules/providers/tests/claude-sdk-permission-mode.test.mjs`:
- Around line 1-30: Add four tests to claude-sdk-permission-mode.test.mjs
exercising the untested branches of resolveSdkPermissionMode: (1) assert that
resolveSdkPermissionMode('default', { skipPermissions: false }, { isRoot: true
}) returns undefined to cover the "no override" path, (2) assert that
resolveSdkPermissionMode('plan', { skipPermissions: false }, { isRoot: true })
returns 'plan', (3) assert that resolveSdkPermissionMode('plan', {
skipPermissions: true }, { isRoot: false }) returns 'plan' to mirror the
existing root-only plan test for non-root sessions, and (4) add a test that
omits the options argument (call resolveSdkPermissionMode('default') or similar)
to exercise the process.getuid fallback/auto-detection behavior; ensure each
test uses the same assert.equal pattern and references resolveSdkPermissionMode.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c56a0bcd-21e7-4f7a-8268-f009dc2c6a33
📒 Files selected for processing (6)
CHANGELOG.mdREADME.mdfeatures.mdpackage.jsonserver/claude-sdk.jsserver/modules/providers/tests/claude-sdk-permission-mode.test.mjs
| function resolveSdkPermissionMode(permissionMode, toolsSettings = {}, options = {}) { | ||
| const isRoot = options.isRoot ?? (typeof process.getuid === 'function' && process.getuid() === 0); | ||
|
|
||
| if (permissionMode === 'bypassPermissions') { | ||
| return isRoot ? 'dontAsk' : 'bypassPermissions'; | ||
| } | ||
|
|
||
| if (toolsSettings.skipPermissions && permissionMode !== 'plan') { | ||
| return isRoot ? 'dontAsk' : 'bypassPermissions'; | ||
| } | ||
|
|
||
| if (permissionMode && permissionMode !== 'default') { | ||
| return permissionMode; | ||
| } | ||
|
|
||
| return undefined; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all callers of queryClaudeSDK to see whether they pre-approve tools or rely on bypass semantics.
rg -nP -C3 'queryClaudeSDK\s*\(' --type=js --type=ts
# And where permissionMode: 'bypassPermissions' is passed anywhere in the server.
rg -nP -C2 "permissionMode\s*:\s*['\"]bypassPermissions['\"]"Repository: siteboon/claudecodeui
Length of output: 2786
🌐 Web query:
Claude Agent SDK permissionMode dontAsk canUseTool callback called
💡 Result:
In the Claude Agent SDK, when permissionMode is set to "dontAsk", the canUseTool callback is not called. Instead, tools that are not pre-approved by allowed_tools, hooks, or permission rules are automatically denied without prompting or invoking the callback. This creates a locked-down agent that only uses explicitly permitted tools, ideal for headless or secure environments.
Citations:
- 1: https://code.claude.com/docs/en/agent-sdk/permissions
- 2: https://hexdocs.pm/claude_code/permissions.html
- 3: https://hexdocs.pm/claude_agent_sdk/ClaudeAgentSDK.Permission.html
- 4: https://hexdocs.pm/claude_agent_sdk/permissions.html
- 5: feat: support
auto(anddontAsk) permission modes from @anthropic-ai/claude-agent-sdk slopus/happy#1156
🏁 Script executed:
# Get full context of queryClaudeSDK calls in agent.js
sed -n '947,960p' server/routes/agent.js
sed -n '970,980p' server/routes/agent.js
# Get full context in git.js
sed -n '980,990p' server/routes/git.js
# Check how resolveSdkPermissionMode is used in queryClaudeSDK
sed -n '491,550p' server/claude-sdk.js | head -80Repository: siteboon/claudecodeui
Length of output: 3240
🏁 Script executed:
# Search for canUseTool callback and how permissionMode is passed to SDK
rg -n 'canUseTool|permissionMode.*=' server/claude-sdk.js | head -20Repository: siteboon/claudecodeui
Length of output: 646
🏁 Script executed:
# Look for any toolsSettings or allowedTools being set before queryClaudeSDK calls
rg -n 'toolsSettings|allowedTools' server/routes/agent.js server/routes/git.jsRepository: siteboon/claudecodeui
Length of output: 47
🏁 Script executed:
# Find mapCliOptionsToSDK implementation
sed -n '150,210p' server/claude-sdk.jsRepository: siteboon/claudecodeui
Length of output: 1878
🏁 Script executed:
# Also verify if there's any allowedTools handling in mapCliOptionsToSDK
rg -n 'mapCliOptionsToSDK' server/claude-sdk.js -A 30Repository: siteboon/claudecodeui
Length of output: 2668
Fix: dontAsk semantics aren't equivalent to bypassPermissions and silently denies tools under root.
Per the Claude Agent SDK documentation, dontAsk denies any tool not pre-approved by allowedTools/settings/hooks; the canUseTool callback is never invoked at the permission-check step. This creates a critical mismatch in the current code:
The problem:
mapCliOptionsToSDKcallsresolveSdkPermissionMode, which transformsbypassPermissions→dontAskwhen running as root (line 45-49).- Calls at
server/routes/agent.js:950–956andserver/routes/agent.js:974–977pass onlypermissionMode: 'bypassPermissions'with notoolsSettingsorallowedTools. - After transformation to
dontAsk,toolsSettingsdefaults to{allowedTools: []}(empty list, line 186–190). - The
canUseToolcallback at line 546 checks for the literal string'bypassPermissions', but sincepermissionModeis now'dontAsk', the early-allow branch never fires. - Result: SDK denies every tool call silently, rather than bypassing permissions.
The same issue affects server/routes/git.js:983–987 and potentially other root-run scenarios.
Minimum required fixes:
- Either avoid the
bypassPermissions→dontAsktransformation, or - Populate
allowedToolswith the actual set of tools the agent may need before callingqueryClaudeSDK, or - Update
canUseToolto also recognize'dontAsk'mode and apply bypass logic (though this inverts the semantic intent ofdontAsk).
Document the intended root-user behavior (if silent tool denial is desired, clarify that in the permission mode semantics; if bypass is intended, fix the transformation).
| import { resolveSdkPermissionMode } from '../../../../dist-server/server/claude-sdk.js'; | ||
|
|
||
| // This test validates the compiled server artifact, so run it through | ||
| // `npm run test:claude-sdk-permissions`, which rebuilds dist-server first. |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check that CI / workflows reference the new npm script or build before tests.
fd -e yml -e yaml . .github/workflows 2>/dev/null | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"' | rg -n -C2 'test:claude-sdk-permissions|node --test|build:server'Repository: siteboon/claudecodeui
Length of output: 47
🏁 Script executed:
# Check if .github/workflows exists and list its contents
ls -la .github/workflows 2>/dev/null || echo "No .github/workflows directory found"Repository: siteboon/claudecodeui
Length of output: 367
🏁 Script executed:
# List top-level .github directory
ls -la .github 2>/dev/null || echo "No .github directory found"Repository: siteboon/claudecodeui
Length of output: 299
🏁 Script executed:
# Check package.json for script definitions
head -100 package.json | grep -A 50 '"scripts"'Repository: siteboon/claudecodeui
Length of output: 1980
🏁 Script executed:
# Look for the specific test file and verify it exists
find . -name "claude-sdk-permission-mode.test.mjs" -type fRepository: siteboon/claudecodeui
Length of output: 134
🏁 Script executed:
cat .github/workflows/discord-release.ymlRepository: siteboon/claudecodeui
Length of output: 728
🏁 Script executed:
cat .github/workflows/docker.ymlRepository: siteboon/claudecodeui
Length of output: 1570
🏁 Script executed:
cat .github/workflows/release.ymlRepository: siteboon/claudecodeui
Length of output: 1423
🏁 Script executed:
# Look for test configuration files
find . -maxdepth 2 -type f \( -name "jest.config.*" -o -name "vitest.config.*" -o -name "mocha.opts" -o -name ".mocharc.*" \) 2>/dev/nullRepository: siteboon/claudecodeui
Length of output: 47
🏁 Script executed:
# Check if there are any other test files
find server/modules/providers/tests -type f -name "*.test.*" -o -name "*.spec.*" 2>/dev/null | head -20Repository: siteboon/claudecodeui
Length of output: 175
🏁 Script executed:
# Search for "test" references in package.json scripts
grep -A 100 '"scripts"' package.json | grep -i testRepository: siteboon/claudecodeui
Length of output: 205
CI workflows don't invoke the test:claude-sdk-permissions script yet.
The script is correctly defined in package.json with the necessary npm run build:server prerequisite, but none of the three CI workflows (discord-release.yml, docker.yml, release.yml) currently run this test. The release workflow publishes to npm without executing any tests, so this regression coverage won't run automatically. Add npm run test:claude-sdk-permissions to the appropriate workflow (likely release.yml before publishing) to ensure the test executes as part of your CI pipeline.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/modules/providers/tests/claude-sdk-permission-mode.test.mjs` around
lines 3 - 6, Add the missing CI step to run the compiled-server test by invoking
the npm script "npm run test:claude-sdk-permissions" in the release workflow
(release.yml) before the publish step; update the workflow to run that script
(it depends on build:server already) so the test file
server/modules/providers/tests/claude-sdk-permission-mode.test.mjs is executed
during CI prior to publishing.
Summary
bypassPermissionstodontAskwhen CloudCLI runs asrootBug scenario
Claude chat fails immediately with
Claude Code process exited with code 1when:root(for example viasystemdon a server)bypassPermissionsmode, or enables Claude skip-permissionsShell mode does not show the same failure because it launches the interactive Claude CLI instead of the SDK stream-json path.
Reproduction
rootbypassPermissions, or enable Claude skip-permissions in settingsClaude Code process exited with code 1Root cause
CloudCLI forwards Claude chat requests through
@anthropic-ai/claude-agent-sdk, which launches Claude Code with stream-json arguments and--permission-mode bypassPermissions.Claude rejects bypass mode when executed as
root/sudo, returning:--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasonsThat stderr is swallowed by the SDK and surfaces in CloudCLI only as a generic exit code 1.
Suggested fix location
The safest place to fix this is the Claude SDK option mapping in:
server/claude-sdk.jsSpecifically, the permission-mode resolution should normalize both:
permissionMode === 'bypassPermissions'When CloudCLI is running as
root, both should map todontAskinstead ofbypassPermissions.Validation
npm run test:claude-sdk-permissionsSummary by CodeRabbit
Release Notes
Bug Fixes
Documentation
Tests