New modules: - server/mailer.mjs: nodemailer transport wrapper - server/mail_perms.mjs: runtime permission store, persisted to disk New actions: - send-email: checks (caller, to, topic) permission before sending - set-mail-permission: grant/revoke permissions, gated by canApprove - get-mail-permissions: list current permissions Handler signature extended to handler(params, ctx) where ctx carries caller, users, mail_perm_store and mailer_send. Existing handlers ignore ctx so the change is backwards-compatible. SMTP config lives in secrets.json under optional 'smtp' key. Mail permissions path via --mail-perms or CONDUIT_MAIL_PERMS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
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 |
Workspace root for path resolution |
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) |
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 }) => {
// do something
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