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

Environment variables:

Variable Default Description
CONDUIT_PORT 3015 Port to listen on
CONDUIT_ROOT /workspace Workspace root for path resolution

Client (container / agent)

ccc-client --secrets agent-secrets.json --user agent '{"action": "list-actions"}'
ccc-client --secrets agent-secrets.json --user agent '{"action": "edit-file", "filename": "/workspace/foo.mjs"}'

--secrets and --user can also be set via environment variables:

export CCC_SECRETS=/path/to/agent-secrets.json
export CCC_USER=agent
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

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

Supports CCC_SECRETS and CCC_USER env vars the same as the client.


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
  • 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%