- server/policy_overrides.mjs: persistent group/action override store - Group toggle: disabling a group forces all its actions to auto-deny without touching individual overrides; re-enabling restores them intact - Action overrides: cycle auto-accept/queue/auto-deny/clear per action - Effective policy resolution in index.mjs: group > action override > hardcoded default - auth.mjs: add check_can_manage_policies (requires canApprove.length > 0) - Three new endpoints: GET /policies, POST /policies/group/:group, POST /policies/action/:action - client/conduit.mjs: get_policies, set_group_policy, set_action_policy - ccc-queue.mjs: policy view (Tab to switch); group/action panel with space/e/←/→ - actions.mjs: group fields on desktop/email/calendar actions; list-actions includes group - config.example.json: add policy_overrides key - Bump version to 1.3.0 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.
Create a config file
Copy config.example.json to config.json and edit it:
{
"secrets": "../secrets.json",
"mail_perms": "mail-perms.json",
"policy_overrides": "policy-overrides.json",
"smtp": {
"host": "smtp.example.com",
"port": 587,
"secure": false,
"auth": { "user": "relay@example.com", "pass": "<password>" },
"from": "agent@example.com"
},
"google_calendar": {
"credentials": "../google-credentials.json",
"token": "../google-token.json"
},
"bind": "127.0.0.1",
"port": 3015
}
All relative paths are resolved from the config file's directory. Optional keys: smtp, mail_perms, policy_overrides, google_calendar, bind, port.
Running
Server (host)
ccc-server --config config.json
ccc-server --config config.json --dry-run
Server flags:
| Flag | Env variable | Description |
|---|---|---|
--config <path> |
CONDUIT_CONFIG |
Path to config file (required) |
--dry-run |
— | Log all action invocations but do not execute them |
Server environment variables (override config file values):
| Variable | Description |
|---|---|
CONDUIT_PORT |
Port to listen on |
CONDUIT_BIND |
Address to bind to |
CONDUIT_ROOT |
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, allow (bool), topic (optional — omit to match any topic) |
get-mail-permissions |
auto-accept | target_user (optional) |
calendar-list-events |
auto-accept | calendar_id (default primary), time_min, time_max, max_results |
calendar-create-event |
queue | summary, start, end (ISO 8601), description, calendar_id |
calendar-update-event |
queue | event_id, calendar_id, summary, start, end, description |
calendar-delete-event |
queue | event_id, calendar_id |
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.
Calendar mutation actions (create, update, delete) are queue policy — they require human approval in the TUI before executing. Listing is auto-accept.
Runtime policy overrides
The TUI includes a Policies view (press [tab] to switch from the queue). It lets you toggle groups on/off and override individual action policies without restarting the server.
┌─ Groups ──────────────────────┐ ┌─ Actions ──────────────────────────────────┐
│ │ │ │
│ > [●] calendar │ │ auto-accept calendar-list-events │
│ [●] email │ │ queue calendar-create-event │
│ [●] desktop │ │ queue * calendar-update-event │
└───────────────────────────────┘ └────────────────────────────────────────────┘
[space] toggle group [→] edit actions [e] cycle policy [tab] queue view
[space]toggles a group on/off. Disabling a group makes all its actionsauto-deny.[→]moves focus to the actions panel;[←]returns to groups.[e]cycles an action's policy override:auto-accept → queue → auto-deny → (clear default) → …- A
*marker means the action has a non-default policy.
Permission model: only users with a non-empty canApprove list can mutate policies. Agents (with canApprove: []) can read but never modify. canApprove itself is never runtime-writable — it is the static root of the trust hierarchy.
Group and action overrides are independent. Disabling a group does not erase action-level overrides. Re-enabling a group restores them intact.
State persists to policy_overrides in config.json (if configured).
Google Calendar setup
Google Calendar support requires OAuth2 credentials from Google Cloud Console.
One-time setup:
- Create a project at Google Cloud Console, enable the Calendar API
- Create an OAuth2 credential (Desktop app type) and download the JSON file
- Run the auth helper to do the consent flow:
ccc-gcal-auth --credentials google-credentials.json --token google-token.json
This opens an authorization URL in the terminal. Open it in your browser, grant access, and the token is written automatically.
- Add
google_calendarto yourconfig.json:
"google_calendar": {
"credentials": "../google-credentials.json",
"token": "../google-token.json"
}
Paths are resolved relative to the config file. Tokens are refreshed automatically and written back to the token file.
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, exec, mailer_send, calendar }
// ctx is optional — omit if unused
// Add group: 'my-group' to the action def to make it togglable in the TUI
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