Files
claude-code-conduit/README.md
mikael-lovqvists-claude-agent dd7743501a Add runtime policy overrides and TUI policy management view
- 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>
2026-05-21 16:01:45 +00:00

11 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 immediately
  • queue — 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 actions auto-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:

  1. Create a project at Google Cloud Console, enable the Calendar API
  2. Create an OAuth2 credential (Desktop app type) and download the JSON file
  3. 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.

  1. Add google_calendar to your config.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
  • canApprove is empty by default — permissions must be explicitly granted
  • Browser URLs are validated to http/https only before being passed to xdg-open
  • All path arguments are resolved against the volume map; traversal outside known volumes is rejected