* feat: add run_as_user for per-project OS-user isolation Implements #496. Adds an optional `run_as_user` field at the project level that, when set, spawns the project's agent command under a different Unix user via `sudo -n -iu <user> --`. This provides OS-level file-system isolation from the supervisor user that runs cc-connect itself: a misbehaving agent can no longer reach files outside the target user's permission scope, even via python -c / shell redirection / other non-hooked code paths. ## What lands in this PR ### Config (config/) - new fields `run_as_user *string` and `run_as_env []string` on each `[[projects]]` entry - validation: rejects root, rejects invalid usernames, rejects use on Windows, enforces POSIX username charset to close a sudo argv injection vector ### Core primitives (core/runas*.go) POSIX-only (Windows stubs provided for API compatibility). - `BuildSpawnCommand(ctx, opts, name, args...)` — wraps exec.Cmd with `sudo -n -iu <user> --preserve-env=<allowlist> -- name args...` when opts.RunAsUser is set; passthrough otherwise - `FilterEnvForSpawn(env, opts)` — strips supervisor env to a minimal allowlist (PATH, LANG, LC_*, TERM) plus any project-level extensions - `VerifyRunAsUserCheap` — the two cheap sudo probes, suitable for per-spawn re-verification (catches sudoers edits between startup and spawn) - `PreflightRunAsUser` — the three go/no-go startup gates from #496: (1) passwordless sudo to target works, (2) target cannot passwordless sudo (if escalation succeeds, captures `sudo -n -l` output in the error message), and (3) target can read AND write work_dir root. Plus a non-fatal descendant scan warning on unreadable/unwritable descendants with prune-paths, 10s timeout, and a 50-entry cap. - `RunIsolationProbe` + embedded runas_probe.sh (//go:embed) — the leak-audit probe codex asked for: spawns a fixed shell script as the target user and parses its output into a structured IsolationReport. Reports effective id/groups/umask/pwd, target-home config presence, cross-user denial attempts for every other configured run_as_user, and supervisor-denial attempts. Never reads file contents, only existence and access. All of these are testable without a real second user via an injectable `SudoRunner` interface; production uses ExecSudoRunner. ### Failure policy Per-spec: all checks are hard go/no-go, not warnings. - Any preflight fatal in any project: cc-connect refuses to start entirely; no half-spawned processes. - Any audit-level `CROSS_LEAKED` (target reads another project user's secrets) or `SUPERVISOR_LEAKED` (target reads the supervisor's secrets) or `WORKDIR_WRITABLE=no`: cc-connect refuses to start. - Descendant-scan findings are warnings (inaccessibility inside a project tree is sometimes intentional). ### Startup wiring (cmd/cc-connect/) - `runRunAsUserStartupChecks` runs preflight + isolation probe for every project with run_as_user set, in parallel, BEFORE any agent or engine is constructed. Logs every outcome via slog. On any fatal, prints the full list and `os.Exit(1)`. - injects project-level run_as_user / run_as_env into the agent's options map before calling `core.CreateAgent`, so agents can opt in without new top-level plumbing. ### Doctor subcommand (cmd/cc-connect/doctor_runas.go) New `cc-connect doctor user-isolation [--project NAME] [--out PATH]` subcommand. Runs preflight + isolation probe for one or all `run_as_user` projects, writes a JSON report per project, and prints a human-readable summary. Default output is `~/.cc-connect/audits/<timestamp>-<project>.json`. Also supports `--print-script` to dump the embedded probe source for inspection (and for running shellcheck against it). ### Claude Code wiring (agent/claudecode/) - `Agent` struct gains `runAsUser` and `runAsEnv` fields - `New()` parses them from the opts map and skips the `exec.LookPath("claude")` check when run_as_user is set (the target user's PATH is what matters, not the supervisor's) - `newClaudeSession` takes `core.SpawnOptions`, runs the per-spawn cheap re-check immediately before building the command, uses `core.BuildSpawnCommand` instead of `exec.CommandContext`, and filters env via `core.FilterEnvForSpawn` ### Docs - new "Running agents as a different Unix user" section in `docs/usage.md` with setup steps, sudoers example, ACL example, environment-propagation migration checklist (PGSSL*, ~/.claude/ settings.json, ~/.pgpass, ~/keys/, ~/.ssh/, MCP binaries, shell profile, language toolchains), opt-out instructions, and failure-mode debug guide - README section with a short configuration example and a pointer to the full docs - `config.example.toml` commented example with setup recipe ### Tests - config: parse run_as_user/run_as_env, reject root, reject invalid chars, Windows rejection, username validator valid/invalid cases - core: BuildSpawnCommand legacy vs. sudo form, FilterEnvForSpawn filtering, VerifyRunAsUserCheap success/no-sudo/escalation-possible/ empty-user, PreflightRunAsUser all-pass/no-sudo/escalation-fatal/ workdir-fatal/descendant-warnings/descendant-cap, probe parser clean/cross-leak-fatal/supervisor-leak-fatal/workdir-not-writable- fatal, shell quoting, other-users filtering, embedded probe script sanity - agent/claudecode: New() parses run_as_user/run_as_env, run_as_user skips LookPath - cmd/cc-connect: default audit dir suffix, printHumanReport doesn't panic on populated report Uses stubbed SudoRunners throughout — no tests require a real second Unix user or real sudo binary. ## Scope Only Claude Code is wired up in this PR. Each non-claudecode agent has its own session/spawn lifecycle (codex re-spawns per turn, cursor has its own state model, etc.) so the sweep needs per-agent reasoning and is deferred to a follow-up. The core primitives land here so each future agent migration is a small, self-contained PR. Follow-up issue tracks: codex, cursor, gemini, iflow, opencode, qoder, acp, pi. Refs: #496 Credit: issue framing via user + codex adversarial review (go/no-go gates, leak-audit probe, env propagation docs, sudo -iu rationale pinning) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(run_as_user): simplify after code review Addresses findings from a parallel code-reuse / code-quality / efficiency review of #498. Behavior unchanged; tests still pass. Highlights: - cache VerifyRunAsUserCheap results for 30s keyed by runAsUser, so rapid-fire messages from the same Slack user don't pay the ~100ms per-spawn sudo tax. Cache evicts on failure so the next spawn re-verifies fresh. Adds ResetVerifyCache for tests + operators. - doctor user-isolation now fans out across projects in parallel (mirroring runas_startup.go) instead of iterating serially. Per-project output is buffered and printed in order so the report stays grouped. - collapse Agent.runAsUser + Agent.runAsEnv into a single core.SpawnOptions field — eliminates the reassemble-at-call-site pattern. - fold core/runas_username.go into core/runas.go (one 17-line file → one helper near its only caller). - IsolationReport.RawOutput is now only populated when the audit had a fatal — clean reports are smaller on disk. - introduce typed string constants for the probe wire format (tagBegin / tagWorkDirReadable / tagCrossLeaked / ...) so the parser and runas_probe.sh share a discoverable contract. - itoa() in tests → strconv.Itoa - contains() helper in tests → slices.Contains - mass comment trim across runas.go / runas_check.go / runas_audit.go: delete WHAT-restates-name comments (~70 lines) while keeping the load-bearing WHY blocks (sudo -n -iu rationale, env propagation reasoning, find expression format note, scan timeout justification). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(runas): drop PATH from default env allowlist Preserving PATH across the sudo boundary defeats the whole point of -i: it overrides the target user's login-profile PATH with the supervisor's, and leaks supervisor work directories (e.g. the cc-connect source tree on the supervisor's PATH) into the isolated session. sudo -i now rebuilds PATH from /etc/profile + the target user's ~/.profile. Binaries the isolated agent needs (like the claude CLI) should be installed system-wide (e.g. /usr/local/bin symlinks) or put on the target user's own shell profile. Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a: partseeker-coder sessions were running with leigh's PATH intact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(engine): propagate run_as_user to per-workspace agents In multi-workspace mode, getOrCreateWorkspaceAgent constructs a fresh opts map for each per-workspace agent instead of inheriting the project-level opts that main.go decorated at startup. As a result, run_as_user (and run_as_env) silently dropped out of the per-workspace spawn path — the project-level config said partseeker-coder but every spawn actually ran as the supervisor. Fix: expose GetRunAsUser/GetRunAsEnv accessors on *claudecode.Agent and copy them across in getOrCreateWorkspaceAgent via interface assertion, mirroring the existing GetModel/GetMode pattern. Two regression tests cover the propagation and the no-propagation case so multi-workspace deployments that don't use run_as_user are unaffected. Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add OAuth credential sharing guidance for run_as_user setups OAuth access tokens expire after a few hours and only get refreshed by active Claude CLI sessions. When the target user has no active session between cc-connect spawns, its token expires and auth fails with 401. Document the symlink + ACL pattern: target user's credentials.json symlinks to the supervisor's copy so both share one auto-refreshed token. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: graceful session shutdown — close stdin before signals The old Close() called context.Cancel() which sends SIGKILL immediately via Go's exec.CommandContext, giving Claude Code zero time to run Stop hooks. This broke claude-mem session summaries (120s timeout) and any other cleanup that depends on a clean exit. New three-phase shutdown: 1. Close stdin (EOF) — Claude Code exits cleanly, running Stop hooks 2. SIGTERM after 30s grace period if still alive 3. SIGKILL after 5s more as last resort Also increases engine-level close timeout from 10s to 45s to accommodate the longer graceful window. Tested: all agent/claudecode and core tests pass. Verified Claude Code exits cleanly on stdin EOF (exit code 0). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 120s graceful close + hold message during session transition Increases the default graceful stop timeout from 30s to 120s to match claude-mem's Stop hook timeout. The wait ends as soon as the process exits — typical shutdowns take seconds, not the full 120s. When a user messages during an idle-reset that triggers a graceful close, they now see "Wrapping up your previous session (usually a few seconds, up to 2 minutes). Your new session will start automatically." instead of a silent delay. The hold message is sent before the blocking close, and the new session starts immediately when the old one finishes. Engine-level close timeout raised to 130s (120s grace + 5s SIGTERM + 5s buffer). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Leigh Stillard <leigh@stillard.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
31 KiB
Usage Guide
Complete guide to using cc-connect features.
Table of Contents
- Session Management
- Permission Modes
- API Provider Management
- Model Selection
- Work Directory Switching (
/dir,/cd) - Feishu Setup CLI
- Weixin (personal) Setup CLI
- Claude Code Router Integration
- Voice Messages (STT)
- Voice Reply (TTS)
- Image and File Send-Back
- Scheduled Tasks (Cron)
- Multi-Bot Relay
- Daemon Mode
- Multi-Workspace Mode
- Web Admin Dashboard (Beta)
- Bridge — External Adapter Access (Beta)
- Configuration Reference
Session Management
Each user gets an independent session with full conversation context. Manage sessions via slash commands:
| Command | Description |
|---|---|
/new [name] |
Start a new session |
/list |
List all agent sessions for this project |
/switch <id> |
Switch to a different session |
/current |
Show current session info |
/history [n] |
Show last n messages (default 10) |
/usage |
Show account/model quota usage (if supported) |
/provider [...] |
Manage API providers |
/model [switch <alias>] |
List available models or switch by alias |
/dir [path] |
Show or switch the agent work directory |
/allow <tool> |
Pre-allow a tool (next session) |
/reasoning [level] |
View or switch reasoning effort (Codex) |
/mode [name] |
View or switch permission mode |
/stop |
Stop current execution |
/help |
Show available commands |
During a session, the agent may request tool permissions. Reply allow / deny / allow all.
You can also configure automatic session rotation after inactivity:
[[projects]]
name = "demo"
reset_on_idle_mins = 60
When enabled, the next normal message after a long idle period starts in a fresh session automatically, without deleting the old session from /list.
Permission Modes
All agents support permission modes switchable at runtime via /mode.
Claude Code Modes
| Mode | Config Value | Behavior |
|---|---|---|
| Default | default |
Every tool call requires approval |
| Accept Edits | acceptEdits / edit |
File edits auto-approved |
| Auto | auto |
Claude decides when to ask for permission |
| Plan Mode | plan |
Claude only plans, no execution |
| YOLO | bypassPermissions / yolo |
All tools auto-approved |
Codex Modes
| Mode | Config Value | Behavior |
|---|---|---|
| Suggest | suggest |
Only trusted commands run without approval |
| Auto Edit | auto-edit |
Model decides when to ask |
| Full Auto | full-auto |
Auto-approve with sandbox |
| YOLO | yolo |
Bypass all approvals and sandbox |
Cursor Agent Modes
| Mode | Config Value | Behavior |
|---|---|---|
| Default | default |
Trust workspace, ask before tools |
| Force (YOLO) | force / yolo |
Auto-approve all |
| Plan | plan |
Read-only analysis |
| Ask | ask |
Q&A style, read-only |
Gemini CLI Modes
| Mode | Config Value | Behavior |
|---|---|---|
| Default | default |
Prompt for approval |
| Auto Edit | auto_edit / edit |
Auto-approve edits |
| YOLO | yolo |
Auto-approve all |
| Plan | plan |
Read-only plan mode |
Qoder CLI / OpenCode / iFlow CLI
| Mode | Config Value | Behavior |
|---|---|---|
| Default | default |
Standard permissions |
| YOLO | yolo |
Skip all checks |
Configuration
[projects.agent.options]
mode = "default"
# allowed_tools = ["Read", "Grep", "Glob"]
Switch at runtime:
/mode # show current and available modes
/mode yolo # switch to YOLO mode
/mode default # switch back
API Provider Management
Switch between API providers at runtime without restart.
Configure Providers
[projects.agent.options]
work_dir = "/path/to/project"
provider = "anthropic" # active provider
[[projects.agent.providers]]
name = "anthropic"
api_key = "sk-ant-xxx"
[[projects.agent.providers]]
name = "relay"
api_key = "sk-xxx"
base_url = "https://api.relay-service.com"
model = "claude-sonnet-4-20250514"
[[projects.agent.providers.models]]
model = "claude-sonnet-4-20250514"
alias = "sonnet"
[[projects.agent.providers.models]]
model = "claude-opus-4-20250514"
alias = "opus"
[[projects.agent.providers.models]]
model = "claude-haiku-3-5-20241022"
alias = "haiku"
# MiniMax — OpenAI-compatible, 1M context
[[projects.agent.providers]]
name = "minimax"
api_key = "your-minimax-api-key"
base_url = "https://api.minimax.io/v1"
model = "MiniMax-M2.7"
# For Bedrock, Vertex, etc.
[[projects.agent.providers]]
name = "bedrock"
env = { CLAUDE_CODE_USE_BEDROCK = "1", AWS_PROFILE = "bedrock" }
CLI Commands
cc-connect provider add --project my-backend --name relay --api-key sk-xxx --base-url https://api.relay.com
cc-connect provider list --project my-backend
cc-connect provider remove --project my-backend --name relay
cc-connect provider import --project my-backend # from cc-switch
Chat Commands
/provider Show current provider
/provider list List all providers
/provider add <name> <key> [url] [model]
/provider remove <name>
/provider switch <name>
/provider <name> Shortcut for switch
Env Var Mapping
| Agent | api_key → | base_url → |
|---|---|---|
| Claude Code | ANTHROPIC_API_KEY |
ANTHROPIC_BASE_URL |
| Codex | OPENAI_API_KEY |
OPENAI_BASE_URL |
| Gemini CLI | GEMINI_API_KEY |
use env map |
| OpenCode | ANTHROPIC_API_KEY |
use env map |
| iFlow CLI | IFLOW_API_KEY |
IFLOW_BASE_URL |
Model Selection
Pre-configure a list of selectable models per provider using [[providers.models]]. Each entry has a model identifier and an optional alias (short name shown in /model).
Configure Models
[[projects.agent.providers]]
name = "openai"
api_key = "sk-xxx"
[[projects.agent.providers.models]]
model = "gpt-5.3-codex"
alias = "codex"
[[projects.agent.providers.models]]
model = "gpt-5.4"
alias = "gpt"
[[projects.agent.providers.models]]
model = "gpt-5.3-codex-spark"
alias = "spark"
Chat Commands
/model List available models (format: alias - model)
/model switch <alias> Switch to the model matching the alias
/model switch <name> Switch to the model by its full name
/model <alias> Legacy syntax, still supported
When models is configured, /model shows exactly that list without making an API round-trip. When omitted, models are fetched from the provider API or fall back to a built-in list.
Work Directory Switching (/dir, /cd)
Switch where the next agent session starts, directly from chat.
Chat Commands
/dir Show current work directory and recent history
/dir <path> Switch to a path (relative or absolute)
/dir <number> Switch to a directory from history
/dir - Switch back to previous directory
/dir help Show command usage
/cd <path> Backward-compatible alias of /dir <path>
Behavior Notes
- Directory changes apply to the next session in the current project.
- Relative paths are resolved from the current agent work directory.
- Directory history is project-scoped and can be switched by index.
/cdis kept for compatibility, but/diris the primary command.
Examples:
/dir ../another-repo
/dir 2
/dir -
Running agents as a different Unix user (run_as_user)
Platform support: Linux and macOS. Not supported on Windows. Agent support: Claude Code today. Other agents fall back to the supervisor user; see the tracking issue for migration status.
What this is
By default, every agent session cc-connect spawns runs as the same Unix
user that runs cc-connect itself. If an agent misbehaves — reads a
secret, overwrites a sibling repo, trashes ~/.ssh/ — it has the
supervisor user's full file-system reach.
run_as_user sets a per-project target Unix user. When it is set,
cc-connect spawns that project's agent command via
sudo -n -iu <target-user> -- claude ...
The target user is a real, unprivileged Unix account that you create. The agent runs under that account's uid/gid, with its own home directory, shell profile, PATH, and tool credentials. File-system isolation is enforced by the kernel, not by hooks or allowlists.
Security guarantee and non-guarantee
This provides OS-user isolation from any file or process the target
user cannot reach. An agent can no longer read or clobber the
supervisor's ~/.ssh/, another project user's ~/.pgpass, or a repo
whose UNIX permissions don't grant access to the target user.
This does not automatically isolate projects from each other if they
share the same run_as_user. If you want per-project isolation, create
a separate Unix user per project.
This is not a sandbox in the sense of Linux namespaces, seccomp, or container isolation. It is strictly file-system scoping by uid.
Setup
1. Create the target user and install their tooling
The target user needs its own copy of everything the agent touches,
because sudo -i loads the target user's login environment — not the
supervisor's.
sudo useradd -m -s /bin/bash partseeker-coder
sudo -iu partseeker-coder
# Install the agent CLI under the target user's PATH
# (for Claude Code, follow the normal install instructions)
# Set up the target user's ~/.claude/
mkdir -p ~/.claude
# Copy or re-create:
# ~/.claude/settings.json (MCP servers, hooks, model settings)
# ~/.claude.json (Claude Code auth)
# ~/.claude/plugins/ (claude-mem and any other plugin state)
exit
2. Grant the supervisor passwordless sudo to the target
Add a scoped sudoers rule. Do not use NOPASSWD: ALL for the
supervisor — that grants the supervisor root, which is irrelevant here
and dangerous.
# /etc/sudoers.d/cc-connect (install with `sudo visudo -f ...`)
partseeker-orchestrator ALL=(partseeker-coder) NOPASSWD: ALL
Adjust the usernames for your setup. The rule says: "the supervisor user may run any command as this specific target user, without a password."
3. Verify the target user cannot sudo
The whole point of stepping down into a target user is that the target cannot immediately escalate back. Verify:
sudo -n -iu partseeker-coder -- sudo -n true
# must FAIL with "a password is required" or similar
If that command succeeds, cc-connect will refuse to start. Remove any
NOPASSWD sudo grants for the target user first.
4. Make the project's work_dir accessible to the target user
The target user needs read AND write on the project's work_dir. If
the directory is owned by the supervisor, either chown it to the
target, add group ownership the target is in, or apply a POSIX ACL:
sudo setfacl -R -m u:partseeker-coder:rwX /home/leigh/workspace/sandboxed-repo
sudo setfacl -R -dm u:partseeker-coder:rwX /home/leigh/workspace/sandboxed-repo
cc-connect refuses to start if the target user cannot read+write the
work_dir root, and warns (non-fatal) for descendant paths that look
inaccessible.
5. Audit the setup before starting cc-connect
cc-connect doctor user-isolation
This runs the full preflight (the three go/no-go gates from
#496) and an
isolation probe: it spawns a fixed shell script as the target user
and reports what the target can read, what it's denied, and any
cross-user leaks. Output goes to stdout plus a JSON report in
~/.cc-connect/audits/<timestamp>-<project>.json.
Exit code 0 = clean. Exit code 1 = at least one fatal problem.
You can inspect the probe script itself with:
cc-connect doctor user-isolation --print-script
Configuration
[[projects]]
name = "claude-sandboxed"
run_as_user = "partseeker-coder"
# Optional: extend the default env var allowlist that crosses the sudo
# boundary. The defaults (PATH, LANG, LC_*, TERM) are always included.
# Only list vars the target user cannot reasonably set in their own
# shell profile. Secrets belong in the target user's ~/.claude/settings.json
# env block, NOT here.
run_as_env = ["PGSSLROOTCERT", "PGSSLMODE"]
[projects.agent]
type = "claudecode"
[projects.agent.options]
mode = "default"
model = "sonnet"
work_dir = "/home/leigh/workspace/sandboxed-repo"
Environment propagation: what moves into the target user's home
This is the 2am-debugging section. When you switch a project to
run_as_user, the supervisor's environment is not forwarded across
the sudo boundary — that's the whole point. Everything the agent needs
has to live in the target user's home.
Migration checklist:
-
Agent config —
~/.claude/settings.json(MCP servers, hooks, model settings),~/.claude.json(auth). Copy from the supervisor or re-create from scratch. -
Plugin state —
~/.claude/plugins/— claude-mem, any other Claude Code plugins. -
MCP server binaries — must be on the target user's
PATH, not just the supervisor's. Either install under the target user or reference full paths insettings.json. -
Postgres TLS —
PGSSLROOTCERT,PGSSLCERT,PGSSLKEYbelong in the target user's~/.claude/settings.jsonenvblock. Their referenced cert files must be readable by the target user. -
Claude OAuth credentials — if you authenticate via
claude.ai(OAuth), the token lives in~/.claude/.credentials.json. OAuth access tokens expire after a few hours and are refreshed automatically by whichever Claude CLI session is running. The target user's token will not be refreshed unless the target user has an active session — which it often doesn't between cc-connect spawns. The recommended fix is to symlink the target user's credentials to the supervisor's file so both share one token that stays fresh:```bash # Grant target user read access via ACL (keeps 600 for everyone else) setfacl -m u:<target-user>:rx ~/.claude/ setfacl -m u:<target-user>:r ~/.claude/.credentials.json # Replace the target user's credentials with a symlink sudo -iu <target-user> bash -c \ 'rm -f ~/.claude/.credentials.json && \ ln -s /home/<supervisor>/.claude/.credentials.json \ ~/.claude/.credentials.json' ``` **If you use an API key** (`ANTHROPIC_API_KEY`) instead of OAuth, this is not an issue — set the key in the target user's `~/.claude/settings.json` `env` block and it won't expire. -
Credential files —
~/.pgpass,~/.gitconfig,~/.netrc,~/.aws/,~/.config/gh/,~/.kube/— whichever the agent actually uses. Each needs its own copy or a group-readable shared copy. -
SSH keys —
~/.ssh/id_ed25519etc., if the agent runsgit pushover SSH. Same story: copy or group-share. -
Key material under
~/keys/— custom directories the supervisor uses need an equivalent under the target user's home or a group-readable shared copy. -
Language toolchains — if the agent uses
asdf,mise,nvm,rustup, etc., those live in~. The target user needs either its own install or a system-wide install that both users can run. -
Shell profile —
~/.profile/~/.bashrcon the target user needs to setPATHand any tool init the agent depends on. Test withsudo -iu partseeker-coderbefore wiring cc-connect.
After migration, run cc-connect doctor user-isolation again. The
target home section reports which expected paths are present and
which are missing — missing isn't necessarily wrong, but it's your
checklist.
Opting out
Remove run_as_user from the project entry, or set it to "". Legacy
behavior (spawn as supervisor) returns on the next restart.
Failure modes and error messages
- "passwordless sudo to user X is not configured" — step 2 of setup
is missing or the sudoers rule is scoped to the wrong supervisor. Fix
the rule, run
visudo -cto validate syntax, then restart cc-connect. - "target user X can run passwordless sudo" — step 3 failed. The
error includes the output of
sudo -lfrom the target context; find the offending rule and remove it. - "target user X cannot read AND write work_dir Y" — step 4 failed.
chownthe directory or add an ACL as shown above. - "CROSS_LEAKED" or "SUPERVISOR_LEAKED" in the audit — the
target user can read another user's secrets. Tighten the offending
file's permissions (usually
chmod 600 file; chown user:user file) and re-audit. - "descendant scan timed out" — non-fatal. The
work_diris large enough that the permission walk exceeded its timeout. Runcc-connect doctor user-isolationmanually if you want the full walk, or narrow the project'swork_dir.
Feishu Setup CLI
Use CLI to create or bind Feishu/Lark bot credentials and write them back to config.toml.
# Recommended: unified entry
cc-connect feishu setup --project my-project
cc-connect feishu setup --project my-project --app cli_xxx:sec_xxx
# Force modes (usually unnecessary)
cc-connect feishu new --project my-project
cc-connect feishu bind --project my-project --app cli_xxx:sec_xxx
Differences:
setup: unified entry. No credentials => behaves likenew; with--app=> behaves likebind.new: force QR onboarding flow; rejects--app.bind: force credential binding flow; requires credentials.
Behavior:
setupuses QR onboarding by default, or bind mode when--appis provided.- If
--projectdoes not exist, it is created automatically. - If project exists but has no
feishu/larkplatform, one is added automatically. - The command writes credentials (
app_id,app_secret); in QR onboarding flow, Feishu usually pre-configures permissions and event subscriptions. - Still verify app publish status and availability scope in Feishu Open Platform.
- Runtime platform config also supports an optional
domainoverride for Feishu/Lark API endpoints; this does not change setup/onboarding URLs.
Weixin (personal) Setup CLI
Weixin personal chat uses the ilink bot HTTP API (long polling + sendMessage, same family as OpenClaw openclaw-weixin). Use the CLI to scan a QR code or bind an existing Bearer token and write config.toml.
Full walkthrough (Chinese): docs/weixin.md.
cc-connect weixin setup --project my-project
cc-connect weixin bind --project my-project --token '<token>'
cc-connect weixin new --project my-project
Notes:
setupwithout--tokenruns QR login; with--tokenbehaves like bind.- Auto-creates the project and/or a
weixinplatform block when missing. - After login, send a message from WeChat once so
context_tokenis cached. - See
cc-connect weixin helpfor flags (--api-url,--cdn-url,--route-tag, etc.).
Claude Code Router Integration
Claude Code Router routes requests to different model providers.
Setup
-
Install:
npm install -g @musistudio/claude-code-router -
Configure
~/.claude-code-router/config.json:
{
"APIKEY": "your-secret-key",
"Providers": [
{
"name": "deepseek",
"api_base_url": "https://api.deepseek.com/chat/completions",
"api_key": "sk-xxx",
"models": ["deepseek-chat", "deepseek-reasoner"],
"transformer": { "use": ["deepseek"] }
}
],
"Router": {
"default": "deepseek,deepseek-chat",
"think": "deepseek,deepseek-reasoner"
}
}
-
Start:
ccr start -
Configure cc-connect:
[projects.agent.options]
router_url = "http://127.0.0.1:3456"
router_api_key = "your-secret-key" # optional
Voice Messages (Speech-to-Text)
Send voice messages — cc-connect transcribes them automatically.
Supported: Feishu, WeChat Work, Telegram, LINE, Discord, Slack
Requirements: OpenAI/Groq API key, ffmpeg
Configure
[speech]
enabled = true
provider = "openai" # or "groq"
language = "" # "zh", "en", or auto-detect
[speech.openai]
api_key = "sk-xxx"
# base_url = ""
# model = "whisper-1"
# [speech.groq]
# api_key = "gsk_xxx"
# model = "whisper-large-v3-turbo"
Install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# macOS
brew install ffmpeg
Voice Reply (Text-to-Speech)
Synthesize AI replies into voice messages.
Supported: Feishu (Lark)
Configure
[tts]
enabled = true
provider = "qwen" # or "openai"
voice = "Cherry"
tts_mode = "voice_only" # "voice_only" | "always"
max_text_len = 0 # 0 = no limit
[tts.qwen]
api_key = "sk-xxx"
# model = "qwen3-tts-flash"
TTS Modes
| Mode | Behavior |
|---|---|
voice_only |
Reply with voice only when user sends voice |
always |
Always send voice reply |
Switch: /tts always or /tts voice_only
Image and File Send-Back
When an agent generates a local image, PDF, report, bundle, or other file and needs to deliver it directly to the current chat, use attachment mode in cc-connect send.
Currently supported platforms:
- Feishu
- Telegram
When to run setup first
If the current agent does not natively inject the system prompt, run this once in chat after upgrading:
/bind setup
or:
/cron setup
These two commands write the same cc-connect instructions. Either one is enough. After that, the agent knows:
- normal text replies should be returned normally
- generated attachments should be sent back with
cc-connect send --image/--file
If you have run setup before, run it again after upgrading so the instructions are refreshed to the latest version.
Config switch
Add this to config.toml if you want to disable agent-driven attachment send-back:
attachment_send = "off"
The default is on. This switch is independent from the agent's /mode and only affects cc-connect send --image/--file.
CLI examples
cc-connect send --image /absolute/path/to/chart.png
cc-connect send --file /absolute/path/to/report.pdf
cc-connect send --file /absolute/path/to/report.pdf --image /absolute/path/to/chart.png
Notes:
--imageis for image attachments.--fileis for any file attachment.--messageis optional and sends a text note before the attachments.--imageand--filecan both be repeated.- Absolute paths are recommended so the command does not depend on the agent's current working directory.
- With
attachment_send = "off", image/file send-back is blocked but ordinary text replies still work.
Typical use cases
- The agent generates a screenshot or chart and should send it directly to the user.
- The agent generates a PDF, Markdown export, log bundle, or patch file that should be delivered as an attachment.
- The agent wants to send a short status message together with one or more generated files.
Important notes
- This command is for attachment delivery, not ordinary text replies.
- The files must exist on the local machine where the agent runs.
- There must be an active session; otherwise the command fails because cc-connect has no chat context to deliver to.
- Platform-specific file size and file type limits still apply.
Scheduled Tasks (Cron)
Create scheduled tasks that run automatically.
Chat Commands
/cron List all jobs
/cron add <min> <hour> <day> <mon> <wk> <prompt> Create job
/cron del <id> Delete job
/cron enable <id> Enable job
/cron disable <id> Disable job
Example:
/cron add 0 6 * * * Summarize GitHub trending repos
CLI Commands
cc-connect cron add --cron "0 6 * * *" --prompt "Summarize GitHub trending" --desc "Daily Trending"
cc-connect cron list
cc-connect cron del <job-id>
Optional: --session-mode new-per-run starts a fresh agent session on each run (default is reuse, same as before). --timeout-mins N sets how long the scheduler waits per run (0 = no limit; omit = 30 minutes).
Natural Language (Claude Code)
"Every day at 6am, summarize GitHub trending"
Claude Code auto-creates the cron job. For other agents that rely on memory files, run /cron setup or /bind setup once first; both write the same instructions.
Multi-Bot Relay
Cross-platform bot communication in group chats.
Group Chat Binding
/bind Show bindings
/bind claudecode Add claudecode project
/bind gemini Add gemini project
/bind -claudecode Remove claudecode
Bot-to-Bot Communication
cc-connect relay send --to gemini "What do you think about this architecture?"
Daemon Mode
Run as background service.
cc-connect daemon install --config ~/.cc-connect/config.toml
cc-connect daemon start
cc-connect daemon stop
cc-connect daemon restart
cc-connect daemon status
cc-connect daemon logs [-f]
cc-connect daemon uninstall
Multi-Workspace Mode
One bot serving multiple workspaces per channel.
Configure
[[projects]]
name = "my-project"
mode = "multi-workspace"
base_dir = "~/workspaces"
[projects.agent]
type = "claudecode"
Commands
/workspace Show current binding
/workspace bind <name> Bind local folder
/workspace init <git-url> Clone and bind repo
/workspace unbind Remove binding
/workspace list List all bindings
How It Works
- Channel name
#project-a→ auto-binds tobase_dir/project-a/ - Each channel has isolated sessions and agent state
Web Admin Dashboard (Beta)
Status: Beta. This feature is available since v1.2.2-beta.5. The UI and API may change in future releases.
A full-featured management UI embedded in the binary — project CRUD, session management, cron job editor, global settings, chat interface, and i18n support.
Quick Setup (Chat Command)
The easiest way to enable web admin:
/web setup
This automatically enables both the Management API and the Bridge in config.toml, generates tokens, and prints the access URL. You may need to run /restart for changes to take effect.
After setup, open the URL shown (default http://localhost:9820) and log in with the token.
Check Status
/web # or /web status — show current web admin URL and status
Manual Configuration
Add the following to config.toml:
[management]
enabled = true
port = 9820 # Management UI & API listen port
token = "your-secret-token" # Login token; /web setup generates one automatically
cors_origins = ["*"] # Allowed CORS origins; empty = no CORS headers
Then restart cc-connect.
Build Options
Web assets are compiled into the binary by default. To exclude them (saves ~1MB):
make build-noweb
# or
go build -tags 'no_web' ./cmd/cc-connect
When built with no_web, the /web command will report that web admin is not available.
Management API
The Management API is served on the same port as the UI. Base URL: http://<host>:<port>/api/v1
All API requests require the Authorization: Bearer <token> header.
Key endpoints:
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/status |
System status (version, uptime, platforms) |
POST |
/api/v1/restart |
Restart cc-connect |
POST |
/api/v1/reload |
Reload configuration |
GET |
/api/v1/projects |
List projects |
GET |
/api/v1/sessions?project=<name> |
List sessions for a project |
GET |
/api/v1/cron |
List cron jobs |
GET |
/api/v1/settings |
Get global settings |
PATCH |
/api/v1/settings |
Update global settings |
Full API reference: management-api.md
Bridge — External Adapter Access (Beta)
Status: Beta. This feature is available since v1.2.2-beta.5. The protocol may change in future releases.
The Bridge exposes a WebSocket + REST server so external adapters (custom UIs, bots, scripts) can interact with cc-connect sessions — send messages, receive events, manage sessions.
Enable via Chat
The /web setup command enables Bridge automatically alongside the Management API.
Manual Configuration
Add the following to config.toml:
[bridge]
enabled = true
port = 9810 # Bridge listen port (separate from management)
token = "your-bridge-secret" # Auth token for WebSocket and REST
path = "/bridge/ws" # WebSocket endpoint path
cors_origins = ["*"] # Allowed CORS origins; empty = no CORS
Then restart cc-connect.
Authentication
All Bridge connections require a token. Supported methods:
- Query parameter:
?token=<bridge-token> - Header:
Authorization: Bearer <bridge-token> - Header:
X-Bridge-Token: <bridge-token>
WebSocket
Connect to:
ws://<host>:<bridge-port>/bridge/ws?token=<bridge-token>
The WebSocket supports bidirectional messaging — send user messages to the agent and receive agent events (text, tool calls, permission requests, etc.) in real time.
REST API
Served on the same port as the WebSocket.
| Method | Path | Description |
|---|---|---|
GET |
/bridge/sessions?session_key=...&project=... |
List sessions |
POST |
/bridge/sessions |
Create a new session |
GET |
/bridge/sessions/{id}?session_key=...&project=... |
Get session detail + history |
DELETE |
/bridge/sessions/{id}?session_key=...&project=... |
Delete a session |
POST |
/bridge/sessions/switch |
Switch active session |
Full protocol reference: bridge-protocol.md
Port Summary
| Service | Default Port | Config Block |
|---|---|---|
| Management (Web UI + API) | 9820 | [management] |
| Bridge (WebSocket + REST) | 9810 | [bridge] |
Configuration Reference
See config.example.toml for full examples.
Project Structure
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode" # or codex, cursor, gemini, qoder, opencode, iflow
[projects.agent.options]
work_dir = "/path/to/project"
mode = "default"
provider = "anthropic"
[[projects.platforms]]
type = "feishu" # or dingtalk, telegram, slack, discord, wecom, weixin, line, qq, qqbot
[projects.platforms.options]
# platform-specific options