chenhg5/cc-connect · critical
passwordless sudo to user %q failed (check that your sudoers
Error message
passwordless sudo to user %q failed (check that your sudoers rule is present and scoped to this user): %w: %s
What it means
VerifyRunAsUserCheap (core/runas.go:244) runs 'sudo -n -iu <user> -- /usr/bin/true' to confirm passwordless sudo to the target run-as user works; failure means the sudoers rule is missing, mis-scoped, requires a password, or the target user doesn't exist. The underlying sudo error and stderr are embedded in the message.
Source
Thrown at core/runas.go:244
//
// Returns nil if both checks behave as expected. Results are cached for
// verifyCacheTTL keyed by runAsUser so rapid-fire messages don't pay the
// ~100ms cost per spawn. A failure evicts the cache immediately so the
// next spawn re-verifies fresh.
//
// The expensive checks (work_dir access, isolation probe) live in the
// preflight and audit packages and only run at startup / via `cc-connect
// doctor user-isolation`.
func VerifyRunAsUserCheap(ctx context.Context, runner SudoRunner, runAsUser string) error {
if runAsUser == "" {
return errors.New("VerifyRunAsUserCheap: runAsUser is empty")
}
if verifyCacheHit(runAsUser) {
return nil
}
if out, err := runner.Run(ctx, "-n", "-iu", runAsUser, "--", "/usr/bin/true"); err != nil {
verifyCacheEvict(runAsUser)
return fmt.Errorf("passwordless sudo to user %q failed (check that your sudoers rule is present and scoped to this user): %w: %s", runAsUser, err, strings.TrimSpace(string(out)))
}
out, err := runner.Run(ctx, "-n", "-iu", runAsUser, "--", "sudo", "-n", "/usr/bin/true")
if err == nil {
verifyCacheEvict(runAsUser)
return fmt.Errorf("target user %q can run passwordless sudo; isolation is meaningless. Remove NOPASSWD sudo for this user. Output: %s", runAsUser, strings.TrimSpace(string(out)))
}
verifyCacheStore(runAsUser)
return nil
}
// verifyCacheTTL is short by design. It absorbs a burst of messages
// (one Slack user typing rapidly) while still re-verifying often enough
// that a sudoers edit during a long idle gap is caught on the next spawn.
const verifyCacheTTL = 30 * time.Second
var (
verifyCacheMu sync.Mutex
verifyCache = map[string]time.Time{}View on GitHub (pinned to 4000b2338a)
Solutions
- Add a sudoers rule granting the supervisor user passwordless sudo to the target: <supervisor> ALL=(<target>) NOPASSWD: ALL in /etc/sudoers.d/
- Verify manually: sudo -n -iu <target> -- /usr/bin/true
- Confirm the target user exists (id <target>) and the username matches config exactly
- Ensure the sudoers rule permits the exact invocation path (if scoped to a command, include /usr/bin/true and sudo itself); run visock/visudo -c to check syntax
- Check for requiretty or sudo password policy that blocks non-interactive use
Example fix
// before: /etc/sudoers has no rule for the supervisor // sudo: a password is required // after: /etc/sudoers.d/cc-connect supervisor ALL=(agentuser) NOPASSWD: ALL
Defensive patterns
Strategy: validation
Validate before calling
func sudoToTargetOK(target string) error {
out, err := exec.Command("sudo", "-n", "-iu", target, "--", "/usr/bin/true").CombinedOutput()
if err != nil { return fmt.Errorf("sudo to %s failed: %w: %s", target, err, strings.TrimSpace(string(out))) }
return nil
}
// call before configuring run_as_user in config.toml Try / catch
sess, err := newClaudeSession(ctx, cfg)
if err != nil {
if strings.Contains(err.Error(), "passwordless sudo to user") {
log.Fatalf("Fix sudoers first: %v\nHint: add '%s ALL=(%s) NOPASSWD: ALL' to /etc/sudoers.d/", err, currentUsername(), cfg.RunAsUser)
}
return err
} Prevention
- Add the sudoers rule as a provisioning step (idempotent file in /etc/sudoers.d) before first start
- Test 'sudo -n -iu <user> -- /usr/bin/true' in CI/deploy smoke tests
- Ensure the rule covers the exact supervisor user the daemon runs as (systemd User=)
- Run visudo -c after any sudoers change
When it happens
Trigger: Called (from newClaudeSession) when starting a run-as-isolated session and the sudo probe exits non-zero: no NOPASSWD rule for the supervisor to the target user, rule limited to specific commands, target username misspelled/nonexistent, sudo asking for a password or a TTY, or the verify cache was expired and re-verified a now-broken setup.
Common situations: Fresh deployment where the sudoers file was never updated; sudoers rule edited to 'user ALL=(ALL) NOPASSWD' but run via a runner binary that doesn't match the rule's command scope; target user removed by provisioning changes; sudoers requires requiretty.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- claudeSession: run_as_user spawn refused: %w
- VerifyRunAsUserCheap: runAsUser is empty
- RunIsolationProbe: RunAsUser is empty
- PreflightRunAsUser: RunAsUser is empty
- project %q: target user %q can run passwordless sudo. The ru
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/6899c02bb7ae69b4.
Report an issue: GitHub.