chenhg5/cc-connect · error

claudeSession: run_as_user spawn refused: %w

Error message

claudeSession: run_as_user spawn refused: %w

What it means

newClaudeSession (agent/claudecode/session.go:347) refuses to spawn Claude Code under run_as_user isolation until the privilege-drop path is proven viable. It runs core.VerifyRunAsUserCheap with a 10-second timeout using a sudo runner; any verification failure (sudo unavailable, wrong password config, target user missing, timeout) is returned as 'claudeSession: run_as_user spawn refused: %w' and the session is cancelled. This is a deliberate pre-flight guard, not a runtime crash.

Source

Thrown at agent/claudecode/session.go:347

	// outerArgs are understood by both the wrapper and Claude CLI directly.
	var outerArgs []string
	if model != "" {
		outerArgs = append(outerArgs, "--model", model)
	}

	slog.Debug("claudeSession: starting", "innerArgs", core.RedactArgs(innerArgs), "outerArgs", core.RedactArgs(outerArgs), "dir", workDir, "mode", mode, "run_as_user", spawnOpts.RunAsUser)

	// Per-spawn defense in depth: if run_as_user is set, re-run the cheap
	// preflight (sudo still works + target still can't escalate) right
	// before we build the command. This catches sudoers being edited
	// between startup preflight and now.
	if spawnOpts.IsolationMode() {
		verifyCtx, verifyCancel := context.WithTimeout(sessionCtx, 10*time.Second)
		err := core.VerifyRunAsUserCheap(verifyCtx, core.ExecSudoRunner{}, spawnOpts.RunAsUser)
		verifyCancel()
		if err != nil {
			cancel()
			return nil, fmt.Errorf("claudeSession: run_as_user spawn refused: %w", err)
		}
	}

	// Build final argument list.
	// When cmdArgsFlag is set (e.g. "-a"), inner args are bundled into a
	// single passthrough string via that flag, while outer args (--model etc.)
	// are appended directly so the wrapper can also interpret them.
	// Args containing spaces/newlines are quoted so the wrapper's command-line
	// parser (e.g. splitCommandLine) keeps them as single tokens.
	// Result: my-cli code -t foo -a "--verbose --append-system-prompt 'long text'" --model x
	var allArgs []string
	if cmdArgsFlag != "" {
		allArgs = append(allArgs, cliExtraArgs...)
		allArgs = append(allArgs, cmdArgsFlag, shellJoinArgs(innerArgs))
		allArgs = append(allArgs, outerArgs...)
	} else {
		allArgs = append(allArgs, cliExtraArgs...)
		allArgs = append(allArgs, innerArgs...)

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Inspect the wrapped cause: 'sudo: command not found' → install sudo; 'a password is required' → add a NOPASSWD sudoers rule.
  2. Add sudoers entry: <cc-connect-user> ALL=(<run-as-user>) NOPASSWD: ALL, then test with sudo -u <run-as-user> true -u as the service user.
  3. Verify the target account exists and is not locked/expired (passwd -S <user>, chage -l <user>).
  4. If isolation isn't needed, remove run_as_user from the claudecode agent config so the plain spawn path is used.
  5. If sudo is just slow, fix PAM/NSS latency (e.g. remove network lookups) so verification fits in 10s.

Example fix

// before (/etc/sudoers.d/cc-connect — missing)
// cc-connect cannot sudo to the run-as user → spawn refused
// after
# /etc/sudoers.d/cc-connect (chmod 0440)
ccbot ALL=(alice) NOPASSWD: /usr/bin/claude, /bin/kill
# validate: sudo visudo -c && sudo -u ccbot sudo -u alice true
Defensive patterns

Strategy: validation

Validate before calling

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := core.VerifyRunAsUserCheap(ctx, core.ExecSudoRunner{}, targetUser); err != nil {
    return fmt.Errorf("run_as_user %s not usable yet: %w", targetUser, err)
}

Try / catch

sess, err := agent.StartSession(ctx, opts)
if err != nil && strings.Contains(err.Error(), "run_as_user spawn refused") {
    return nil, fmt.Errorf("isolation preflight failed; verify sudoers grants for %q before starting sessions: %w", opts.RunAsUser, err)
}

Prevention

When it happens

Trigger: Calling StartSession when spawnOpts.IsolationMode() is true (run_as_user configured) and VerifyRunAsUserCheap fails within 10s: sudo binary missing, sudoers not permitting NOPASSWD for the cc-connect user, target run_as_user account doesn't exist or is locked, or sudo prompts for a password (blocking until timeout).

Common situations: Deploying with run_as_user set but forgetting the sudoers entry ('ccbot ALL=(target) NOPASSWD: ...'); running cc-connect in a container without sudo installed; target user created with an expired/locked account; slow sudo/PAM making the 10s verify timeout fire.

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/43ba004c24c3cbab. Report an issue: GitHub.