@northbound-run/agent-wrap
v0.3.0
Published
Analyze a codebase, detect skills/agents/MCP servers, and bundle into a Docker container running Claude Code as a headless service
Readme
agent-wrap
Analyze a codebase, detect its skills, agents, and MCP servers, generate a config file, and bundle everything into a Docker container that runs Claude Code as a headless service.
Core principle: Claude Code IS the agent runtime. agent-wrap wraps it, not replaces it. You prototype locally in Claude Code, then agent-wrap build bundles the same environment into a deployable container. The experience is 1:1.
Quickstart
# In your project directory
bunx @northbound-run/agent-wrap init # Analyze codebase, generate config
bunx @northbound-run/agent-wrap validate # Check config + skill commands
bunx @northbound-run/agent-wrap dev # Run locally via Docker Compose
bunx @northbound-run/agent-wrap build # Build production Docker image
bunx @northbound-run/agent-wrap deploy fly # Deploy to Fly.io (or railway, render, ssh)How it works
agent-wrap initscans your project for CLI entry points (skills), MCP servers, and agent definitions- Generates
agent-wrap.config.yamlwith detected capabilities and confidence scores agent-wrap buildproduces a Docker image with your project + Claude Code CLI installed- The container exposes a REST API for autonomous tasks and direct skill execution
POST /api/task --> Claude Code runs autonomously (needs LLM API key)
POST /api/skills/ --> Direct skill execution (no LLM needed)
POST /webhook --> Webhook ingestion (HMAC auth, async callbacks)Two interaction modes
Autonomous mode (primary)
POST a task prompt and Claude Code executes it with full access to your project's tools, MCP servers, and AGENTS.md -- exactly as if running claude -p "do this" locally.
# Create a task
curl -X POST http://localhost:3000/api/task \
-H "X-API-Key: $AGENT_WRAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Transcribe meeting.m4a and generate a summary report"}'
# Returns: {"taskId": "task_a1b2c3d4"}
# Check status
curl http://localhost:3000/api/task/task_a1b2c3d4 \
-H "X-API-Key: $AGENT_WRAP_API_KEY"
# Stream progress via SSE
curl http://localhost:3000/api/task/task_a1b2c3d4 \
-H "X-API-Key: $AGENT_WRAP_API_KEY" \
-H "Accept: text/event-stream"
# Cancel a running task
curl -X POST http://localhost:3000/api/task/task_a1b2c3d4/cancel \
-H "X-API-Key: $AGENT_WRAP_API_KEY"Direct mode (no LLM needed)
Call detected skills directly via REST -- no Claude API key required. Useful for deterministic operations like transcription, PDF generation, or image processing.
curl -X POST http://localhost:3000/api/skills/transcribe \
-H "X-API-Key: $AGENT_WRAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "base.en"}'Config reference
agent-wrap init generates agent-wrap.config.yaml. Key sections:
schema_version: 3
name: my-project
version: "0.1.0"
language: typescript # or python
runtime: oven/bun:1 # Docker base image
skills:
transcribe:
description: "Transcribe audio files"
command: ["bun", "run", "src/cli/transcribe.ts", "--model", "${model}"]
inputs:
- name: model
type: string
default: "base.en"
outputs:
type: text
timeout: 300
concurrency: 1
# LLM provider config -- env vars passed to Claude Code
llm:
env:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"
# Override for alternative providers:
# ANTHROPIC_BASE_URL: "${MIMO_BASE_URL}"
# ANTHROPIC_AUTH_TOKEN: "${MIMO_API_KEY}"
# Claude Code settings
claude_code:
version: "" # Pin Claude Code version (empty = latest)
max_budget_usd: 5.00 # Cost cap per task
task_timeout: 1800 # Wall-clock timeout (seconds)
max_concurrent_tasks: 5 # Max parallel autonomous tasks
append_system_prompt: "" # Additional instructions for Claude Code
permission_mode: "bypassPermissions" # Required for headless containers
mcp_config: ".mcp.json" # MCP config forwarded to Claude Code
channels:
enabled: [] # Official channels -> --channels
development: [] # Dev channels -> --dangerously-load-development-channels
permission_prompt_tool: "" # Optional MCP tool for relayed approvals
# Server settings
server:
port: 3000
idle_timeout: 1800 # Auto-shutdown after idle (0 = disable)
max_upload_size: "500mb"
max_output_capture: "10mb"
# Auth
auth:
type: api_key
header: "X-API-Key"
# Key set via AGENT_WRAP_API_KEY env var at runtime
# Webhook channel (separate port, HMAC auth)
channel:
webhook:
port: 3001
hmac:
algorithm: sha256
header: "X-Webhook-Signature"
replay_protection:
max_age_seconds: 300
# Lifecycle hooks
hooks:
pre_execute: []
post_execute: []
on_error: []
# Session state passthrough
session:
enabled: false
header: "X-Session-Params"
env_prefix: "SESSION_"Language support
TypeScript/Bun
Detects skills from package.json scripts, commander/yargs option definitions, and process.argv destructuring patterns.
Python
Detects skills from pyproject.toml [project.scripts], setup.py console_scripts, Click decorators (@click.option, @click.argument), and argparse add_argument calls.
Auto-detection
agent-wrap init detects:
| What | Source | Notes |
|---|---|---|
| Skills | package.json scripts, pyproject.toml, CLI frameworks | Confidence-scored; >= 0.5 = active |
| MCP servers | .mcp.json, mcp.config.json | Claude Code reads these natively |
| Claude channels | .claude/settings*.json, channel-capable .mcp.json servers | Detects official plugin channels and local dev channels |
| Agents | AGENTS.md, CLAUDE.md | Claude Code reads these natively |
| System deps | Native module imports | Mapped to apt packages |
Re-running init on an existing config appends new detections as commented blocks -- it never modifies existing entries.
Webhook integration
The webhook adapter runs on a separate port (default 3001) with HMAC-SHA256 authentication:
# Generate signature
TIMESTAMP=$(date +%s)
BODY='{"type":"task","payload":{"prompt":"Run tests"}}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "$AGENT_WRAP_WEBHOOK_SECRET" | cut -d' ' -f2)
curl -X POST http://localhost:3001/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Timestamp: $TIMESTAMP" \
-H "X-Webhook-Signature: sha256=$SIGNATURE" \
-H "X-Reply-URL: https://my-server.com/callback" \
-d "$BODY"Webhook payloads use type to route: "task" for autonomous execution, "skill" for direct skill calls.
LLM provider configuration
The llm.env section supports ${VAR} interpolation from the container environment:
# Default: Anthropic API
llm:
env:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"
# Alternative provider (e.g., Xiomi/Mimo)
llm:
env:
ANTHROPIC_BASE_URL: "${MIMO_BASE_URL}"
ANTHROPIC_AUTH_TOKEN: "${MIMO_API_KEY}"
ANTHROPIC_MODEL: "mimo-v2-pro"LLM env vars are passed only to Claude Code processes, never to skill child processes.
Native Claude Code channels
agent-wrap can now forward native Claude Code channel flags from YAML:
claude_code:
mcp_config: ".mcp.json"
channels:
enabled:
- plugin:discord@claude-plugins-official
development:
- server:webhook
permission_prompt_tool: "reply"Notes:
enabledmaps toclaude --channels ...developmentmaps toclaude --dangerously-load-development-channels ...- Native Claude channels require a Claude.ai login. Anthropic's docs say Console/API-key-only auth is not sufficient for channels.
- When channel config is present, agent-wrap checks
claude auth statusand fails early if Claude Code is not logged in withauthMethod: "claude.ai". - For Docker/dev deployments, make sure the container has access to a pre-authenticated Claude home (for example by mounting
~/.claudefor the runtime user).
Security model
- REST auth: API key via header (timing-safe comparison).
/healthand/readyare unauthenticated. - Webhook auth: HMAC-SHA256 over
{timestamp}.{body}, with replay protection (300s window). - SSRF protection: Webhook reply URLs to private IPs (10.x, 172.16-31.x, 192.168.x, 127.x) are blocked unless explicitly allowlisted.
- Process isolation: Skills run with
shell: false, stdin closed, filtered env (allowlist only). - LLM var isolation: API keys and LLM env vars are never passed to skill child processes.
- Container security: Non-root
appuser(UID 1001), output capture capped at 10MB.
Docker
Build
bunx @northbound-run/agent-wrap build # Builds agent-wrap/{name}:{version}
bunx @northbound-run/agent-wrap build -t my-image:latest # Custom tagLocal dev
bunx @northbound-run/agent-wrap dev # Generates docker-compose.yml + starts with live reloadWhen dev mode is running, agent-wrap also serves a small browser terminal at
http://localhost:3000/__dev (or your configured server port). The dev UI reuses
the task execution/streaming flow but stays outside the authenticated /api/*
surface, so it works even when AGENT_WRAP_API_KEY is set without exposing that
key to browser JavaScript.
The generated dev compose file now mounts:
- your project-local
.claude/into/app/.claude - your user Claude home
${HOME}/.claudeinto/home/appuser/.claude
That gives Claude Code access to repo settings/plugins plus your host Claude.ai login state, which is required for native channels.
Run
docker run -p 3000:3000 -p 3001:3001 \
-e AGENT_WRAP_API_KEY=your-api-key \
-e AGENT_WRAP_WEBHOOK_SECRET=your-secret \
-e ANTHROPIC_API_KEY=your-anthropic-key \
agent-wrap/my-project:0.1.0Deploying to production
agent-wrap deploy generates platform-specific config for Fly.io, Railway, Render, or any SSH-reachable host:
bunx @northbound-run/agent-wrap deploy fly # Generate fly.toml
bunx @northbound-run/agent-wrap deploy railway # Generate railway.toml
bunx @northbound-run/agent-wrap deploy render # Generate render.yaml
bunx @northbound-run/agent-wrap deploy ssh --hostname app.example.com --ssh-host [email protected]See DEPLOY.md for detailed guides per platform.
API endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health | None | Health check |
| GET | /ready | None | Readiness probe (Claude Code available) |
| GET | /openapi.json | None | OpenAPI 3.1 schema |
| POST | /api/task | API key | Create autonomous task |
| GET | /api/task/:id | API key | Get task status (JSON or SSE) |
| POST | /api/task/:id/cancel | API key | Cancel running task |
| POST | /api/skills/:name | API key | Execute skill directly |
| POST | /webhook | HMAC | Webhook ingestion (port 3001) |
Development
bun install # Install dependencies
bun test # Run test suite (73 tests)
bunx tsc --noEmit # Type checkArchitecture
Event Sources: Channel Dispatch: Handlers:
REST adapter -> -> Claude Code Runner (autonomous)
Webhook adapter -> validate -> route -> execute -> Skill Executor (direct, no LLM)All interactions flow through a unified channel dispatch layer. REST and webhook are event source adapters feeding into the same dispatch. This means one auth layer, one interop surface, and every capability gets both REST and webhook access automatically.
License
MIT
npm publishing
This package is configured to publish to npm as @northbound-run/agent-wrap from the GitHub repository Northbound-Run/agent-wrap.
Publishing is handled by .github/workflows/publish.yml and is triggered by pushing a version tag like v0.1.0.
One-time npm setup
Configure npm trusted publishing for this package. npm only lets you attach a trusted publisher after the package already exists, so the very first release may need a one-off manual publish (or a temporary granular publish token) before switching fully to trusted publishing.
Configure npm trusted publishing for this package:
- GitHub organization/user:
Northbound-Run - Repository:
agent-wrap - Workflow filename:
publish.yml
Release flow
# 1. Bump package.json version
# 2. Commit and push your branch
# 3. Create and push a matching tag
git tag v0.1.0
git push origin main --follow-tagsThe workflow verifies that the Git tag matches package.json, runs bun test, previews the package with npm pack --dry-run, then publishes with npm publish.
