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:
- Check 1 ensures
cc-connect can actually spawn as the target user.
- Check 2 ensures the target user cannot immediately bypass the boundary with passwordless
sudo.
- 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.
Problem
cc-connectcurrently spawns all agent sessions under the Unix user that runscc-connectitself. 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 inconfig.tomlto specify an optionalrun_as_userfield.cc-connectcontinues to run as a supervisor/orchestrator user, but whenrun_as_useris set it launches that project's agent command under the configured target Unix user via passwordlesssudoto that specific user.Spawning behavior:
run_as_useris omitted, current behavior is unchanged: spawn as the supervisor user.run_as_useris set, spawn the agent command as that target user viasudo -n -iu <run_as_user> -- <agent command>.-iis intentional: the spawned process should use the target user's home directory and login environment, not the supervisor user's home.cc-connectmust not blindly preserve the supervisor user's environment. Do not usesudo -Eor any equivalent "forward everything" behavior.~/.profile,~/.bashrc,~/.config, tool credentials, etc.).Security guarantee:
run_as_user.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-connectaborts startup globally and spawns no agents.1. Passwordless
sudoto the target user is configuredVerify the supervisor user can switch to the target user without a password prompt:
This permission must be scoped to the specific target user. Access to arbitrary users or root is not acceptable.
Example sudoers entry:
If the check fails,
cc-connectrefuses to start and prints a project-specific error:2. Target user must not have passwordless
sudoThe point of
run_as_useris to step down into a less-privileged account. If that account can immediately escalate back via passwordlesssudo, 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:
If it succeeds,
cc-connectrefuses to start and prints a fatal error:If retrievable, include
sudo -n -loutput 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_dirWhen
cc-connectspawns the agent, it changes into the project'swork_dir. If that directory is not accessible to the target user, the agent will fail in confusing ways at runtime.Checks:
work_dirroot.EACCESduring normal work.Minimum fatal root check:
For the warning scan, the goal is to surface likely permission problems early, not to perfectly model every possible repo layout or
.gitignorerule. A practical first pass is enough:.git/,node_modules/,.venv/,venv/,dist/,build/,target/,.pytest_cache/, and__pycache__/.Example warning:
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_userfield added to the[[projects]]schema inconfig.tomlrun_as_useris omittedsudo -n -iu <run_as_user> -- <agent command>sudoto the target user is verified; fail with a specific error if missingsudo; fail with a specific error if it canwork_dirroot is fatalcc-connectaborts startup globally and spawns no agentsrun_as_usersudo -n -iuwork_dirroot is inaccessiblerun_as_useris unsetOut Of Scope
chroot, container isolation, or MAC frameworks such as SELinux/AppArmor[[projects]]entry.gitignoreinterpretation across arbitrary nested repo layoutsNotes
These startup checks form a defense-in-depth chain:
cc-connectcan actually spawn as the target user.sudo.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.