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

  1. Add a sudoers rule granting the supervisor user passwordless sudo to the target: <supervisor> ALL=(<target>) NOPASSWD: ALL in /etc/sudoers.d/
  2. Verify manually: sudo -n -iu <target> -- /usr/bin/true
  3. Confirm the target user exists (id <target>) and the username matches config exactly
  4. 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
  5. 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

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


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/6899c02bb7ae69b4. Report an issue: GitHub.