Skip to content

fix(exec): use is_elevated() instead of bare-name check after resolve_program - #165

Merged
github-actions[bot] merged 1 commit into
mainfrom
claude/focused-goodall-35uyde
Nov 4, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
claude/focused-goodall-35uyde

Conversation

@brujack

@brujack brujack commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Bug

resolve_program() (added in #159) returns a full absolute path like /usr/bin/sudo, not a bare command name. The elevation gate that follows it checks:

if command.eq("doas") || command.eq("sudo") || command.eq("run0") {
    self.elevate()?;  // sudo --validate with inherited stdin
}

Since "/usr/bin/sudo" != "sudo", this condition is always false. self.elevate() — which calls sudo --validate with inherited stdin to warm the credential cache — is never called for any privileged step.

Impact

Trigger: a privileged: true non-streaming command.run step runs when sudo credentials are expired.

Before #159: elevate() called sudo --validate with Stdio::inherit() first, letting the user enter their password. The subsequent .output() call (stdin not inherited) succeeded because the cache was warm.

After #159 (broken): elevate() skipped entirely. The .output() call runs sudo directly without inherited stdin; sudo cannot prompt → fails with "sudo: a password is required".

Streaming mode is less affected (stdin is inherited for the actual run), but the credential pre-check is still missing.

Root Cause

resolve_program was inserted between elevate_if_required() and the bare-name check without updating the check to account for the full-path return value.

Fix

Replace the bare-name string comparison with self.is_elevated(), which is the exact predicate that was intended (privileged=true && not root) and works correctly regardless of how the provider binary is resolved.

// Before (broken after resolve_program returns full path):
if command.eq("doas") || command.eq("sudo") || command.eq("run0") {

// After:
if self.is_elevated() {

Validation

  • New test resolve_program_returns_full_path_not_bare_name asserts the return value is an absolute path, locking in the assumption that makes the bare-name check wrong.
  • All exec module tests pass (the five elevate_* unit tests that check elevate_if_required() fail in this root environment because elevation_needed(true, "root") = false — pre-existing, unrelated to this change, confirmed by running them on main before the fix).


Generated by Claude Code

…_program

resolve_program() returns a full absolute path (e.g. /usr/bin/sudo), not a
bare command name. The original check

    if command.eq("doas") || command.eq("sudo") || command.eq("run0") { ... }

was introduced in #159 without updating for the new full-path return value.
The comparison always evaluates to false, so self.elevate() (which calls
`sudo --validate` with inherited stdin to warm the credential cache) is never
called for privileged steps.

Trigger: a privileged: true non-streaming command.run step runs with expired
sudo credentials. Because elevate() is not called first, sudo runs with stdin
piped (not inherited, Command::output() default), cannot prompt, and fails
with "sudo: a password is required" rather than prompting the user.

Fix: replace the bare-name check with self.is_elevated(), which is the exact
semantic that was intended (privileged=true && not root) and covers all three
providers (sudo, doas, run0) correctly regardless of their resolved path.

Regression test: resolve_program_returns_full_path_not_bare_name asserts
the return value is an absolute path so this bug cannot silently re-appear.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKLTCW3D6caXjkjvCiL
@github-actions
github-actions Bot merged commit 0ae3de8 into main Nov 4, 2026
12 of 13 checks passed
@brujack
brujack deleted the claude/focused-goodall-35uyde branch November 5, 2026 01:26
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.

2 participants