- Add send-email, set-mail-permission, get-mail-permissions to actions table - Document --mail-perms flag and CONDUIT_MAIL_PERMS env var - Correct CONDUIT_ROOT description (informational only, not path resolution) - Add ctx argument to "adding actions" example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.9 KiB
claude-code-conduit
A supervised action bridge between Claude Code and the host system. Claude requests structured actions; the server applies per-action policies and optionally holds them for human approval before executing.
Concepts
Actions are typed verbs with named parameters — not shell commands. The server defines what actions exist and what happens when they are called. Example:
{ "action": "edit-file", "filename": "/workspace/foo.mjs" }
Policies control what happens when an action is requested:
auto-accept— executed immediately (e.g. open a file in the editor)auto-deny— rejected immediatelyqueue— held for human approval (e.g. open a browser URL)
Authentication uses HMAC-SHA256. Every request is signed with the caller's secret. Secrets live in a JSON file — never in environment variables.
Users each have a secret and a canApprove list controlling whose queued actions they may approve.
Install
# Global install from Gitea
npm install -g git+https://gitea.efforting.tech/mikael-lovqvists-claude-agent/claude-code-conduit.git
# Or clone and link locally
git clone git@git.efforting.tech:mikael-lovqvists-claude-agent/claude-code-conduit.git
cd claude-code-conduit
npm install
npm link
Generate secrets
# Create a secrets file with random secrets for each user
ccc-keygen --create user,agent
# Edit secrets.json to configure who can approve whom:
# set user.canApprove = ["agent"]
# Produce a filtered file for the agent (e.g. to copy into a Docker container)
ccc-keygen --filter agent --output agent-secrets.json
The full secrets.json stays on the host. agent-secrets.json goes into the container.
Running
Server (host)
ccc-server --secrets secrets.json
ccc-server --secrets secrets.json --mail-perms mail-perms.json
Server flags:
| Flag | Env variable | Description |
|---|---|---|
--secrets <path> |
— | Path to secrets file (required) |
--bind <addr> |
CONDUIT_BIND |
Address to bind to (default 127.0.0.1) |
--mail-perms <path> |
CONDUIT_MAIL_PERMS |
File to persist mail permissions (optional; in-memory only if omitted) |
Server environment variables:
| Variable | Default | Description |
|---|---|---|
CONDUIT_PORT |
3015 |
Port to listen on |
CONDUIT_BIND |
127.0.0.1 |
Address to bind to |
CONDUIT_ROOT |
/workspace |
Label printed at startup (informational only — path resolution uses VOLUME_MAPPING in server/helpers.mjs) |
Client (container / agent)
ccc-client --secrets agent-secrets.json --user agent --url http://192.168.2.99:3015 '{"action": "list-actions"}'
ccc-client --secrets agent-secrets.json --user agent '{"action": "edit-file", "filename": "/workspace/foo.mjs"}'
--secrets, --user, and --url can also be set via environment variables:
export CCC_SECRETS=/path/to/agent-secrets.json
export CCC_USER=agent
export CONDUIT_URL=http://192.168.2.99:3015
ccc-client '{"action": "list-actions"}'
The JSON payload can be spread across multiple arguments — they are space-joined before parsing:
ccc-client '{"action": "edit-file",' '"filename": "/workspace/foo.mjs"}'
Queue manager (host)
ccc-queue --secrets secrets.json --user user --url http://192.168.2.99:3015
Opens an interactive TUI showing pending actions:
┌─ Pending Actions ──────────┐ ┌─ Details ────────────────────────────┐
│ │ │ │
│ > [a1b2c3] open-browser │ │ Action: open-browser │
│ [d4e5f6] open-terminal │ │ ID: a1b2c3d4-... │
│ │ │ Submitted by: agent │
│ │ │ Created: 2026-03-07T12:00:00Z │
│ │ │ │
│ │ │ Params: │
│ │ │ url: https://example.com │
└────────────────────────────┘ └──────────────────────────────────────┘
[y] approve [n] deny [r] refresh [q] quit
Client environment variables:
| Variable | Default | Description |
|---|---|---|
CCC_SECRETS |
— | Path to secrets file |
CCC_USER |
— | Username to authenticate as |
CONDUIT_URL |
http://localhost:3015 |
Server URL |
Actions
Query available actions at runtime:
ccc-client '{"action": "list-actions"}'
Built-in actions:
| Action | Policy | Params |
|---|---|---|
list-actions |
auto-accept | — |
edit-file |
auto-accept | filename (path) |
open-browser |
queue | url (http/https only) |
open-terminal |
queue | path (optional, defaults to workspace) |
send-email |
auto-accept | to, subject, body, topic |
set-mail-permission |
auto-accept | target_user, to, topic, allow (bool) |
get-mail-permissions |
auto-accept | target_user (optional) |
send-email checks that the caller has a mail permission entry matching (caller, to, topic) before sending. Permissions are managed via set-mail-permission, which requires the caller to have canApprove over the target user — so only humans can grant/revoke permissions for agents.
Adding actions
Edit server/actions.mjs. Each entry needs:
'my-action': {
description: 'What this does',
params: [{ name: 'foo', required: true, type: 'string' }],
policy: 'auto-accept', // or 'auto-deny' | 'queue'
handler: ({ foo }, ctx) => {
// ctx = { caller, users, mail_perm_store, mailer_send }
// ctx is optional — omit the second argument if you don't need it
return { result: foo };
},
},
Path resolution
The server translates container-side paths to host-side paths using the volume map in server/helpers.mjs. By default this matches the docker-compose.yml layout:
| Container path | Host path |
|---|---|
/workspace |
<CONTAINER_PATH>/workspace |
/home/claude |
<CONTAINER_PATH>/claude-home |
Paths outside known volumes are rejected. Edit CONTAINER_PATH and VOLUME_MAPPING in server/helpers.mjs to match your setup.
Security notes
- Secrets are never passed via environment variables or command line arguments — only via a file
- HMAC signatures include a timestamp; requests older than 30 seconds are rejected
canApproveis empty by default — permissions must be explicitly granted- Browser URLs are validated to
http/httpsonly before being passed toxdg-open - All path arguments are resolved against the volume map; traversal outside known volumes is rejected