mikael-lovqvists-claude-agent 64df986a5f Thread exec and mailer_send through ctx instead of importing directly
- actions.mjs no longer imports exec from helpers; uses ctx.exec instead
- index.mjs builds ctx via make_ctx(), which injects dry-run stubs for
  exec and mailer_send when --dry-run is active
- Handlers now run fully (including permission checks) in dry-run mode;
  only the actual side effects are stubbed out

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:58:00 +00:00
2026-03-17 22:37:14 +00:00
2026-03-17 22:46:56 +00:00
2026-03-17 22:41:09 +00:00

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.


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)
--dry-run Log all action invocations but do not execute them

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
  • 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
Description
A supervised action bridge between Claude Code and the host system
Readme 140 KiB
Languages
JavaScript 100%