Skip to content

fix: avoid Claude chat exit when bypass mode runs as root#702

Open
michaeledi wants to merge 1 commit intositeboon:mainfrom
michaeledi:fix/claude-root-bypasspermissions-chat
Open

fix: avoid Claude chat exit when bypass mode runs as root#702
michaeledi wants to merge 1 commit intositeboon:mainfrom
michaeledi:fix/claude-root-bypasspermissions-chat

Conversation

@michaeledi
Copy link
Copy Markdown

@michaeledi michaeledi commented Apr 24, 2026

Summary

  • map Claude chat bypassPermissions to dontAsk when CloudCLI runs as root
  • apply the same safeguard to the Claude-specific skip-permissions toggle
  • add regression coverage for the root permission-mode mapping
  • document the runtime behavior and test command

Bug scenario

Claude chat fails immediately with Claude Code process exited with code 1 when:

  • CloudCLI runs as root (for example via systemd on a server)
  • the user enables the chat mode button's bypassPermissions mode, or enables Claude skip-permissions
  • the Claude provider is used through the chat UI (SDK path)

Shell mode does not show the same failure because it launches the interactive Claude CLI instead of the SDK stream-json path.

Reproduction

  1. Run CloudCLI as root
  2. Open a Claude chat session
  3. Switch the chat mode button to bypassPermissions, or enable Claude skip-permissions in settings
  4. Send any message
  5. Observe that the request fails immediately with Claude Code process exited with code 1

Root 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 reasons

That 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.js

Specifically, the permission-mode resolution should normalize both:

  • explicit chat permissionMode === 'bypassPermissions'
  • Claude skip-permissions settings

When CloudCLI is running as root, both should map to dontAsk instead of bypassPermissions.

Validation

  • npm run test:claude-sdk-permissions

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed Claude chat failures when CloudCLI runs as root. Permission bypass requests now map to "do not ask" mode to prevent SDK errors.
  • Documentation

    • Added clarification on permission mode behavior when running with root privileges.
  • Tests

    • Added test coverage for permission-mode resolution in Claude chat.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Addresses a Claude SDK issue where running as root caused chat requests to exit with code 1. Implements a permission-mode resolver that maps both skip-permissions and bypassPermissions to dontAsk for root sessions, adds a test suite to verify the behavior, and updates documentation.

Changes

Cohort / File(s) Summary
Documentation Updates
CHANGELOG.md, README.md, features.md
Added changelog entry, usage documentation, and release notes describing the Claude chat permission-mode mapping fix for root execution and the new test command.
Build Configuration
package.json
Added new npm script test:claude-sdk-permissions to build the server and run permission-mode tests via Node's test runner.
Core Permission Handling
server/claude-sdk.js
Introduced new exported resolveSdkPermissionMode() function that determines SDK permission mode based on input settings and root detection, replacing direct mode forwarding logic.
Permission Mode Tests
server/modules/providers/tests/claude-sdk-permission-mode.test.mjs
New automated test suite validating the permission-mode resolver for root vs. non-root sessions and various permission mode combinations.

Possibly related PRs

Suggested reviewers

  • viper151

Poem

🐰 A root runs deep, but Claude won't ask,
dontAsk now shields from permission's task,
No exit code one shall plague the way,
Permission modes resolved today!
Tests verify the fix is right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: preventing Claude chat from exiting when bypass permissions mode runs as root by mapping bypassPermissions to dontAsk.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-level test (or test: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) and CHANGELOG.md. If features.md is 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 → expect undefined (the function's "no override" path at Line 60 of claude-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 options entirely) to exercise the process.getuid fallback.
➕ 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 mapping bypassPermissions to dontAsk for root execution.

The README explains that root execution converts bypassPermissions to dontAsk, but the behavioral implications aren't clear. Per the Claude Agent SDK docs, these modes have opposite approval semantics: bypassPermissions auto-approves all tools, while dontAsk denies any tool not explicitly pre-approved via allowedTools/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

📥 Commits

Reviewing files that changed from the base of the PR and between f6200e3 and aa81b91.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • README.md
  • features.md
  • package.json
  • server/claude-sdk.js
  • server/modules/providers/tests/claude-sdk-permission-mode.test.mjs

Comment thread server/claude-sdk.js
Comment on lines +45 to +61
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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 -80

Repository: 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 -20

Repository: 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.js

Repository: siteboon/claudecodeui

Length of output: 47


🏁 Script executed:

# Find mapCliOptionsToSDK implementation
sed -n '150,210p' server/claude-sdk.js

Repository: 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 30

Repository: 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:

  • mapCliOptionsToSDK calls resolveSdkPermissionMode, which transforms bypassPermissionsdontAsk when running as root (line 45-49).
  • Calls at server/routes/agent.js:950–956 and server/routes/agent.js:974–977 pass only permissionMode: 'bypassPermissions' with no toolsSettings or allowedTools.
  • After transformation to dontAsk, toolsSettings defaults to {allowedTools: []} (empty list, line 186–190).
  • The canUseTool callback at line 546 checks for the literal string 'bypassPermissions', but since permissionMode is 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:

  1. Either avoid the bypassPermissionsdontAsk transformation, or
  2. Populate allowedTools with the actual set of tools the agent may need before calling queryClaudeSDK, or
  3. Update canUseTool to also recognize 'dontAsk' mode and apply bypass logic (though this inverts the semantic intent of dontAsk).

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).

Comment on lines +3 to +6
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 f

Repository: siteboon/claudecodeui

Length of output: 134


🏁 Script executed:

cat .github/workflows/discord-release.yml

Repository: siteboon/claudecodeui

Length of output: 728


🏁 Script executed:

cat .github/workflows/docker.yml

Repository: siteboon/claudecodeui

Length of output: 1570


🏁 Script executed:

cat .github/workflows/release.yml

Repository: 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/null

Repository: 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 -20

Repository: siteboon/claudecodeui

Length of output: 175


🏁 Script executed:

# Search for "test" references in package.json scripts
grep -A 100 '"scripts"' package.json | grep -i test

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant