Skip to content

Add optional run_as_user per project for OS-user isolation #496

@leighstillard

Description

@leighstillard

Problem

cc-connect currently spawns all agent sessions under the Unix user that runs cc-connect itself. There is no OS-level enforcement preventing a coding agent from modifying files outside its intended scope: other repos, the supervisor user's home directory, or shared knowledge bases.

Hooks exist but can be bypassed by an agent that knows what it is doing, for example by writing files through python -c, shell redirection, or other non-hooked code paths. Unix user separation cannot be bypassed the same way without an additional privilege-escalation path.

The goal of this change is OS-user isolation from the host/supervisor account. It is not automatic project-to-project isolation. If multiple projects share the same run_as_user, those projects are not isolated from each other. Users who want project-level isolation can create separate Unix users and assign one per project.

Proposed Solution

Allow each [[projects]] entry in config.toml to specify an optional run_as_user field. cc-connect continues to run as a supervisor/orchestrator user, but when run_as_user is set it launches that project's agent command under the configured target Unix user via passwordless sudo to that specific user.

[[projects]]
name = "data-worklog-PM"
run_as_user = "leigh"   # PM agent runs as the human user

[[projects]]
name = "claude"
run_as_user = "partseeker-coder"   # coding agents run as a sandboxed Unix user

Spawning behavior:

  • If run_as_user is omitted, current behavior is unchanged: spawn as the supervisor user.
  • If run_as_user is set, spawn the agent command as that target user via sudo -n -iu <run_as_user> -- <agent command>.
  • -i is intentional: the spawned process should use the target user's home directory and login environment, not the supervisor user's home.
  • cc-connect must not blindly preserve the supervisor user's environment. Do not use sudo -E or any equivalent "forward everything" behavior.
  • If specific environment variables must cross the boundary, they should be passed via an explicit allowlist only.
  • The target user is responsible for its own shell/profile/tool configuration (~/.profile, ~/.bashrc, ~/.config, tool credentials, etc.).

Security guarantee:

  • This provides OS-user isolation from files and secrets that the target user cannot access.
  • It does not isolate projects that share the same run_as_user.
  • Users who want stronger isolation can configure a separate Unix user per project.

Startup Safety Checks

All checks run for all configured projects in parallel before any agent is spawned. If any fatal check fails for any project, cc-connect aborts startup globally and spawns no agents.

1. Passwordless sudo to the target user is configured

Verify the supervisor user can switch to the target user without a password prompt:

sudo -n -iu <run_as_user> -- true

This permission must be scoped to the specific target user. Access to arbitrary users or root is not acceptable.

Example sudoers entry:

partseeker-orchestrator ALL=(partseeker-coder) NOPASSWD: ALL

If the check fails, cc-connect refuses to start and prints a project-specific error:

ERROR: project "claude" requires passwordless sudo to user "partseeker-coder", but it is not configured.
Add a sudoers rule that allows user "partseeker-orchestrator" to run commands as "partseeker-coder" without a password, for example:
  partseeker-orchestrator ALL=(partseeker-coder) NOPASSWD: ALL
Then restart cc-connect.

2. Target user must not have passwordless sudo

The point of run_as_user is to step down into a less-privileged account. If that account can immediately escalate back via passwordless sudo, the isolation boundary is meaningless.

This check is intentionally narrow. We only fail if the target user can perform non-interactive, passwordless privilege escalation.

Verify that the following command fails:

sudo -n -iu <run_as_user> -- sudo -n true

If it succeeds, cc-connect refuses to start and prints a fatal error:

ERROR: target user "partseeker-coder" can run passwordless sudo.
The user-spawn sandbox provides no isolation if the spawned agent can escalate non-interactively.
Remove NOPASSWD sudo access for this user before starting cc-connect with this project.

If retrievable, include sudo -n -l output from the target-user context in the error to help the operator remove the offending rule.

3. Target user must have practical access to the project's work_dir

When cc-connect spawns the agent, it changes into the project's work_dir. If that directory is not accessible to the target user, the agent will fail in confusing ways at runtime.

Checks:

  • Fatal: verify the target user can enter the work_dir root.
  • Warning-only: scan for common descendant access failures that are likely to produce EACCES during normal work.

Minimum fatal root check:

sudo -n -iu <run_as_user> -- test -r <work_dir> -a -x <work_dir>

For the warning scan, the goal is to surface likely permission problems early, not to perfectly model every possible repo layout or .gitignore rule. A practical first pass is enough:

  • Walk the reachable tree as the target user.
  • Warn on unreadable files, unwritable files, unreadable directories, and unsearchable directories.
  • Prune common generated/noisy paths such as .git/, node_modules/, .venv/, venv/, dist/, build/, target/, .pytest_cache/, and __pycache__/.
  • Cap the warning output to the first 50 paths and print a summary count for the remainder.

Example warning:

WARNING: project "claude" work_dir "/home/leigh/workspace" contains paths that user "partseeker-coder" may not be able to access cleanly:
  /home/leigh/workspace/some-repo/secret.env (not readable, mode 600 owner leigh)
  /home/leigh/workspace/another-repo/.git/config (not readable, mode 600 owner leigh)
  ... and 17 more

The agent may fail with EACCES when accessing these paths. Fix ownership/permissions, narrow the project scope, or accept the risk if the inaccessible paths are intentionally out of bounds.

This descendant scan is a warning, not a fatal error. Inaccessibility inside the tree is sometimes intentional. The important thing is to surface it explicitly so the operator is not debugging mystery permission errors later.

Acceptance Criteria

  • run_as_user field added to the [[projects]] schema in config.toml
  • Existing behavior preserved when run_as_user is omitted
  • Spawning uses sudo -n -iu <run_as_user> -- <agent command>
  • Spawned process uses the target user's home/login environment rather than the supervisor user's home/login environment
  • Supervisor environment is not forwarded wholesale; only an explicit allowlist may cross the boundary
  • Startup check: passwordless sudo to the target user is verified; fail with a specific error if missing
  • Startup check: target user cannot run passwordless sudo; fail with a specific error if it can
  • Startup check: inability to enter work_dir root is fatal
  • Startup check: descendant permission scan warns, prunes noisy/generated paths, and caps output
  • All startup checks run per project, in parallel, before any agent is spawned
  • If any project fails a fatal startup check, cc-connect aborts startup globally and spawns no agents
  • Documentation updated with example sudoers config
  • Documentation updated with target-user creation/setup steps
  • Documentation updated with environment/setup expectations for the target user
  • Documentation updated with a migration note for existing users
  • Tests cover config parsing for run_as_user
  • Tests cover spawn invocation with sudo -n -iu
  • Tests cover environment allowlist vs non-allowlisted variables
  • Tests cover fatal failure when passwordless sudo-to-target is missing
  • Tests cover fatal failure when the target user has passwordless sudo
  • Tests cover fatal failure when work_dir root is inaccessible
  • Tests cover warning behavior and capped output for descendant access failures
  • Tests cover global startup abort when any project is misconfigured
  • Tests cover legacy behavior when run_as_user is unset

Out Of Scope

  • Per-channel user binding
  • Linux user namespaces, chroot, container isolation, or MAC frameworks such as SELinux/AppArmor
  • Automatic project-to-project isolation when multiple projects share one Unix user
  • Per-repo separate users inside a single [[projects]] entry
  • Cold-start session behavior
  • Perfect .gitignore interpretation across arbitrary nested repo layouts

Notes

These startup checks form a defense-in-depth chain:

  1. Check 1 ensures cc-connect can actually spawn as the target user.
  2. Check 2 ensures the target user cannot immediately bypass the boundary with passwordless sudo.
  3. Check 3 ensures the target user can actually operate inside the configured work_dir.

Skipping any of them weakens the feature materially. They should all run before any agent starts so that misconfigurations are caught immediately and predictably.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions