No description
  • TypeScript 98.1%
  • CSS 0.8%
  • Just 0.5%
  • Shell 0.3%
  • Dockerfile 0.1%
Find a file
charles 6cc5cf220c
All checks were successful
qa / sql-layer-check (push) Successful in 10s
qa / dockerfile (push) Successful in 11s
qa / i18n-string-check (push) Successful in 12s
qa / db-schema (push) Successful in 1m5s
qa / qa-1 (push) Successful in 2m21s
qa / qa (push) Successful in 0s
ci: bump forge-base to v0.4.3 (fixes corrupted v0.4.x)
2026-06-13 08:19:31 +00:00
.cursor/skills/forgejo-deps-backfill chore: remove all legacy/backward-compat code 2026-05-07 20:44:53 +00:00
.forgejo/workflows ci: bump forge-base to v0.4.3 (fixes corrupted v0.4.x) 2026-06-13 08:19:31 +00:00
.husky chore: sync pre-push hook to forge-base canonical (calls just qa) 2026-05-12 14:11:51 +00:00
apps refactor(architect): remove /breakdown webhook, architect creates issues directly (#1224) 2026-05-16 09:19:45 +00:00
design refactor: rename foreman → architect across code, URLs, schema 2026-05-02 19:31:00 +02:00
docs docs: overhaul README + strip host-mode architect references 2026-05-16 12:14:55 +02:00
flows/defaults refactor(architect): remove /breakdown webhook, architect creates issues directly (#1224) 2026-05-16 09:19:45 +00:00
ops/audit feat(audit): log external docker stop/rm on claude-hooks-* via auditd 2026-04-20 14:33:06 +00:00
packages/shared feat(shared): roster-assistant proposal contract (#1210) 2026-05-15 18:12:32 +00:00
scripts feat(i18n): auto-run paraglide codegen via PostToolUse hook 2026-05-13 15:53:34 +02:00
skills refactor(architect): remove /breakdown webhook, architect creates issues directly (#1224) 2026-05-16 09:19:45 +00:00
specs spec(architect): update specs/agents-roster-ai-assistant.md via Planner 2026-05-15 16:54:07 +00:00
vendor/penpot-mcp-server refactor(service-config): URL consolidation series — PRs A + B + queued cleanup (#827) 2026-05-04 17:36:56 +00:00
.cursorignore fix(cursor): wrap cwd in symlink dir so SDK doesn't blow HTTP/2 frame (#1027) 2026-05-09 22:00:39 +00:00
.dockerignore chore(infra): migrate Docker base + CI workflows to forge-base v0.1.0 2026-05-03 13:11:09 +02:00
.gitignore test(web): migrate component tests to Vitest browser mode (#1014) 2026-05-09 17:04:26 +00:00
biome.json fix(web): resolve pre-existing Biome lint violations blocking push 2026-05-13 15:53:34 +02:00
bun.lock feat(roster-assistant): assistant prompt + Claude call with structured output (#1223) 2026-05-16 08:59:11 +00:00
claude-hooks.service fix(reconcile): docker start stopped-but-matching containers (#188) 2026-04-20 21:02:39 +00:00
CLAUDE.md docs: overhaul README + strip host-mode architect references 2026-05-16 12:14:55 +02:00
Dockerfile feat(i18n): auto-run paraglide codegen via PostToolUse hook 2026-05-13 15:53:34 +02:00
justfile revert: remove forge-base justfile imports (just import does not allow overrides) 2026-06-12 22:52:57 +00:00
package.json chore(biome): upgrade @biomejs/biome v1 → v2.4.13 (#408) 2026-04-26 23:32:53 +00:00
README.md docs: overhaul README + strip host-mode architect references 2026-05-16 12:14:55 +02:00
tsconfig.base.json feat(m18): reshape repo into Bun + Turbo workspace (#171) 2026-04-20 17:50:03 +00:00
tsconfig.json feat(m18): reshape repo into Bun + Turbo workspace (#171) 2026-04-20 17:50:03 +00:00
turbo.json feat(settings): inline settings tree in sidenav + flyout on collapsed rail (#1109) 2026-05-11 18:19:43 +00:00

claude-hooks

Webhook-driven orchestration service that ingests Forgejo / GitHub / GitLab events, routes them through a flow-graph dispatcher, and spawns AI agent workers inside per-role Docker containers — turning issues into pull requests, and PRs into merged code, automatically.


Architecture

┌──────────────────────────────────────────────────┐
│        Forge  (Forgejo / GitHub / GitLab)        │
│    system webhook → POST /webhook/forgejo         │
└────────────────────┬─────────────────────────────┘
                     │ HMAC-verified
                     ▼
┌──────────────────────────────────────────────────┐
│                 claude-hooks                     │
│                                                  │
│  ┌────────────┐  ┌──────────────┐  ┌──────────┐ │
│  │  HTTP API  │  │ Flow runner  │  │  SQLite  │ │
│  │  (Bun)     │  │  (dispatch)  │  │  (state) │ │
│  └────────────┘  └──────┬───────┘  └──────────┘ │
│                         │ FIFO queue per agent    │
│  ┌──────────────────────┴──────────────────────┐ │
│  │           Worker registry                   │ │
│  │  code-lead · dev · reviewer · designer      │ │
│  │  design-reviewer  ·  architect               │ │
│  └──────────────────────┬──────────────────────┘ │
└─────────────────────────│────────────────────────┘
                          │ docker exec
                          ▼
┌──────────────────────────────────────────────────┐
│             Agent containers                     │
│                                                  │
│  ┌──────────┐ ┌─────┐ ┌──────────┐ ┌──────────┐ │
│  │code-lead │ │ dev │ │ reviewer │ │ designer │ │
│  └──────────┘ └─────┘ └──────────┘ └──────────┘ │
│                                                  │
│  ┌────────────────────────────────────────────┐  │
│  │  AI provider  (per agent, operator choice) │  │
│  │  Anthropic · DeepSeek · Ollama · Cursor    │  │
│  └────────────────────────────────────────────┘  │
│  Forge MCP · per-agent credentials               │
└──────────────────────────────────────────────────┘

The service runs under systemd user scope (systemctl --user) on the host machine, listening on port 4500. Every agent — including the architect — runs in its own long-lived Docker container via docker exec.

Each agent type can be configured to use a different AI provider (Anthropic, DeepSeek, Ollama, or Cursor) via the dashboard or agents.json. The orchestration layer is provider-agnostic — only the container's env vars change.


Agent roles

Role Forge user Model Purpose
code-lead code-lead Opus 4.7 Senior implementation — architecture-heavy tasks. Owns the breakdown skill.
dev dev Sonnet 4.6 Implementation — well-scoped, straightforward tasks.
reviewer reviewer Sonnet 4.6 Code review. Squash-merges when APPROVED + CI green + mergeable.
designer designer Opus 4.7 UI/UX mockups in Penpot (MCP-connected).
design-reviewer design-reviewer Sonnet 4.6 Visual-defect review of Penpot frames and dashboard PRs.
architect architect Opus 4.7 Planning + spec-writing. Driven via /architect/* chat endpoints. Singleton. Cannot merge PRs.

Tech stack

Layer Choice
Runtime Bun
Agent SDK @anthropic-ai/claude-agent-sdk (programmatic Claude Code)
ORM Drizzle on bun:sqlite
Frontend Vite + React 19 + TanStack + Tailwind 4
Linter / Formatter Biome
CI Forgejo Actions
Monorepo Bun workspaces + Turbo
Agent runtime Per-instance Docker container

Workspace layout

apps/
  server/      # Bun HTTP service — webhook ingress, flow runner, worker registry
  web/         # React 19 SPA dashboard
  forge-mcp/   # Forge-neutral MCP server baked into every agent container
packages/
  shared/      # Cross-app TypeScript types (TaskRecord, ResolvedAgent, SSE envelopes)

Both apps depend on @claude-hooks/shared via workspace:*. The server boots on port 4500 and serves the React SPA on /app/*. Requests to / and /dashboard receive 302 → /app/ so the SPA boots and runs its entry beforeLoad hook — it probes /onboarding/should-redirect for a fresh fleet, then redirects to /$locale/planner/board using the active locale from settings (default en).


Quick start

just setup      # bun install at the root — links every workspace
just dev        # turbo run dev — server (:4500) + web (:5173) in parallel
just start      # run server in production (matches systemd unit)
just qa         # typecheck + lint + format check + tests across the workspace

If /app/* returns a 503 with a "run cd apps/web && bun run build" hint, the SPA bundle hasn't been built yet. just dev builds as a side effect; just start expects the dist bundle to already exist.

Dev loop (Vite on port 5173, proxies REST/SSE to 4500):

just dev              # turbo runs server + Vite together
# then open http://127.0.0.1:5173/app/planner/board

Dashboard

The React 19 + TanStack + Tailwind 4 dashboard is the sole UI. It carries a hardened SSE reliability path (heartbeat + debounced "disconnected" banner + /health probe).

SPA pages

All routes live under the locale prefix (/app/<locale>/…; en is the default). The /$locale/ landing redirects to /$locale/planner/board.

Path Description
/app/planner/board Assignment board — drag-to-assign Kanban, one column per agent type + an Unassigned gutter. Live via SSE.
/app/planner Architect chat — sessions list + streaming turns. Slash-command palette (/spec, /breakdown, /assign) and @file / @specs/foo.md autocomplete.
/app/workspace Unified workspace — combined specs editor + architect chat. URL params: ?session=<id>, ?spec=<name>, ?repo=<owner/name>.
/app/specs Spec browser — list and open specs/*.md from watched repos.
/app/flows Flow-graph editor — view, create, and edit dispatch flow definitions.
/app/agents Agent CRUD — create / edit / delete instances with volume-wipe confirm.
/app/issue/:owner/:repo/:issueNumber Issue detail — per-issue pipeline stages, task history, and event log.
/app/settings Settings hub — entry point for all configuration surfaces.
/app/settings/service Service config (forge URL, container image, pipeline thresholds, auth).
/app/settings/service/watchdogs Per-stage stall thresholds.
/app/settings/service/ai-providers AI provider configuration and failover.
/app/settings/service/forge Per-forge OAuth credentials.
/app/settings/service/container Container image and runtime settings.
/app/settings/service/design Penpot / design-reviewer settings.
/app/settings/agents Agent-type config and per-instance overrides.
/app/settings/agents/per-agent-secrets Per-agent secret injection.
/app/settings/agents/config-history Agent config change history.
/app/settings/agent-types Agent-type defaults (models, skills, plugins).
/app/settings/agent-config Global agent configuration.
/app/settings/repos Watched repos — add/remove, rotate webhook secrets.
/app/settings/labels Label catalog management.
/app/settings/secrets Encrypted secret store.
/app/settings/language UI language selection (Paraglide i18n).
/app/settings/appearance Theme / colour-scheme preferences.
/app/settings/voice-input Voice input configuration (requires local speaches service).
/app/login Login page (Authelia-backed).
/app/onboarding First-run wizard — forge credentials, webhook registration, initial agent setup.

Assignment board

/app/planner/board is the drag-to-assign Kanban view. One column per agent type, plus a synthetic Unassigned gutter that surfaces open type:user-story issues without an assignee. Cards group by status (Running → Queued → Idle-assigned → Unassigned); the column header reports busy/capacity saturation.

Drag-drop is plain HTML5: dropping a card invokes POST /board/assign, which PATCHes the Forgejo issue's assignees — Forgejo then fires the normal issues.assigned webhook so dispatch routing stays in one place. Optimistic UI: the card moves immediately and rolls back on a non-2xx response. Filters (repo / milestone / label / only-unassigned) bind to URL search params so views are shareable. Live updates via /events invalidate the TanStack Query cache; a 30 s poll backstops idle tabs.

Pipeline stages

The issue detail view (/app/issue/:owner/:repo/:issueNumber) surfaces the canonical stage pipeline — Breakdown → Implement → PR → CI → Review ↺ → Approved → Merge → Closed — derived from GET /issues/pipeline.

State Description
success Stage completed cleanly
running Agent actively working
pending Waiting to start
failure Stage errored
stalled Running/pending past the configured threshold
round > 1 Reviewer REQUEST_CHANGES loop
force-merge Terminator fired after max review rounds

Per-stage stall thresholds are configurable in Settings → Service → Watchdogs (ci_threshold_ms default 15 min, review_threshold_ms default 10 min, implement_threshold_ms default 30 min).

Quick actions for a stalled PR:

  • Bounce — POSTs to /pipeline/bounce-review; the server DELETE + re-POSTs every requested reviewer on the PR so Forgejo fires a fresh pull_request_review_request event and normal routing takes over. Auth-gated (operator-only).
  • Re-trigger CI is a deep-link to the Forgejo Actions run, not an auto-retrigger — safety rail.

Containerised workers

Each code-flow agent runs inside a dedicated long-lived Docker container:

Container Volume Purpose
claude-hooks-code-lead claude-hooks-code-lead-state Code Lead agent
claude-hooks-dev claude-hooks-dev-state Dev agent
claude-hooks-reviewer claude-hooks-reviewer-state Reviewer agent
claude-hooks-designer claude-hooks-designer-state Designer agent (Penpot MCP)
claude-hooks-design-reviewer claude-hooks-design-reviewer-state Design-reviewer agent (Penpot MCP)

The designer / design-reviewer containers embed the Penpot MCP server. Code-only agents (code-lead / dev / reviewer) deliberately do not embed it.

Credentials and isolation

  • Agent code (Claude Agent SDK) runs inside the container via docker exec.
  • Each container has an empty $HOME — no host credentials are visible inside.
  • The Forge token for each agent is injected as FORGEJO_ACCESS_TOKEN at container start, read from ~/.config/claude-hooks/tokens/<name> on the host. Token files are never bind-mounted into containers.
  • Claude Code OAuth credentials are bind-mounted read-only from a dedicated host path into $CLAUDE_CONFIG_DIR/.credentials.json inside the container. Rotating credentials on the host propagates without a container restart.

State persistence

Each agent's cache/, worktrees/, and session data live in the named volume. Worktrees and sessions survive container restarts; an in-flight task will fail on a container stop/start and can be retried.


Forge MCP (apps/forge-mcp/)

forge-mcp is the MCP server baked into every agent container. It replaced the upstream forgejo-mcp Go binary and is compiled to a standalone binary at /usr/local/bin/forge-mcp via bun build --compile.

Provider-neutral by design. Every tool is named mcp__forge__<verb> regardless of which forge backs the dispatch. The right adapter is selected at container start from FORGE_TYPE — agents never branch on forge type.

agent (AI CLI)
  └── stdio JSON-RPC
       └── forge-mcp  (mcp__forge__* tool surface)
            └── ForgePort adapter  (forgejo / github / gitlab)
                 └── REST API

The same ForgePort + adapter code used by the service's own dispatch path backs the MCP — one implementation, no duplicated REST logic.

Tool surface (full list in apps/forge-mcp/src/tools.ts):

Category Tools
Issues get_issue, list_issues, create_issue, update_issue, add_issue_labels, remove_issue_labels
Comments list_comments, create_comment, get_comment, edit_comment, delete_comment
Pull requests get_pull_request, list_pull_requests, create_pull_request, update_pull_request, get_pull_request_diff, list_pull_request_files, merge_pull_request
Reviews list_reviews, get_review, submit_review, dismiss_review, list_review_comments, request_reviewers, remove_review_request
Files read_file, list_dir, write_file
CI list_workflow_runs, get_workflow_run, get_aggregate_ci_status, get_job_logs
Dependencies get_blockers, get_blocked, add_blocker
Labels / milestones list_repo_labels, create_label, list_repo_milestones
Account list_my_repos

See docs/forge-mcp.md for the full tool reference and migration guide from the legacy mcp__forgejo__* namespace.


Architect agent

The architect is a singleton planning + spec-writing agent that runs in the claude-hooks-architect-default container via docker exec, the same mechanism as every other agent. It writes specs, dry-runs /breakdown, and hands work off to the code-lead / dev / reviewer pool.

File operations (spec reads/writes, attach picker) go through the Forge REST adapter — the architect never touches the host filesystem directly. All orchestration (HTTP endpoints, SSE, SQLite session table, steer / abort channels, tool policy) lives in the service process; the Claude Agent SDK subprocess runs inside the container.

What it cannot do (enforced by canUseTool in apps/server/src/domain/agent/architect.ts):

  • Push directly to main / mastergit push origin main patterns in a Bash tool call are denied.
  • Merge PRs — mcp__forgejo__merge_pull_request is refused outright.

Dispatch surface. Forge webhook paths are blocked from reaching the architect — resolveAgentByUser / resolveAgentByType skip non-dispatchable types, so assigning architect as an issue assignee is a silent no-op. The only entry point is the /architect/* chat endpoints.

Chat API:

Method Path Description
POST /architect/chat { session_id?, message } — one turn. Returns { session_id, task_id }.
GET /architect/sessions List sessions (id, title, turn count, preview).
GET /architect/sessions/:id Full transcript (user + assistant messages).
DELETE /architect/sessions/:id Drop a session permanently.
GET /architect/stream/:task_id SSE stream filtered to the given task's events.

Example — start a new session, stream the response:

curl -s -X POST http://localhost:4500/architect/chat \
  -H 'Content-Type: application/json' \
  -d '{"message":"Draft specs/my-feature.md from these notes…"}'
# → { "session_id": "<uuid>", "task_id": "<uuid>", "worker": "architect-default" }

curl -N http://localhost:4500/architect/stream/<task_id>
# → streams: init → progress → tool_call → assistant → result

Provisioning on first deployment:

  1. Create the Forge account (architect), issue a personal access token with write:repository scope, and write it to ~/.config/claude-hooks/tokens/architect (mode 0600).

  2. Seed the architect's Claude Code credentials:

    just agent-env-sync architect
    
  3. Restart the service — ensureDefaultForTypes creates the architect-default row automatically on first boot.

Singleton guarantee: POST /agents refuses a second architect instance with 409 Conflict. The first row is seeded automatically.


Multi-forge & multi-repo

The service watches every row in the watched_repos table (editable via Settings → Repos). Every webhook payload's repository.full_name is matched against that list — unknown repos are rejected with 404. Each row carries owner/name and a forge enum (forgejo / github / gitlab).

An empty watched_repos table is the back-compat escape hatch — the guard is skipped and every repo is accepted. New deployments should always populate it.

Adding a repo

  1. Ensure the agents' Forge tokens can see the repo. Tokens are read from ~/.config/claude-hooks/tokens/<type> at container start.

  2. Add the repo via Settings → Repos → Add with the matching forge enum.

  3. Bootstrap labels:

    just labels-bootstrap <owner>/<repo>   # ad-hoc, no restart needed
    
  4. Register the webhook on the new repo pointing at http://<host>:4500/webhook/forgejo with the shared webhook secret.

  5. Verify routing: assign an issue to an agent user (e.g. dev). The service logs should show [webhook] event=issues action=assigned repo=<owner>/<repo>.


Label-based routing

issue labelled area:design          → designer
designer posts handoff comment
  → issue gets area:design-review   → design-reviewer
PR labelled area:dashboard          → design-reviewer (beats author mapping)
everything else                     → assignee-based routing

Tickets must not carry area:design / area:design-review if they are implementation tasks — use area:agents or a relevant functional label instead.


Design review flow

Two triggers, one agent (design-reviewer), one skill (skills/design-review.md):

  1. Dashboard PR review — trigger: PR labelled area:dashboard or authored by designer. The skill reads the PR diff and verifies every hex literal in the changed files against design/tokens.json. Verdict ships as a Forge pull review (APPROVED or REQUEST_CHANGES).

  2. Penpot handoff review — trigger: issue labelled area:design-review after a designer handoff comment. The skill exports every Penpot frame as PNG, inspects each visually, posts a grouped-findings comment, and removes the area:design-review label.


Systemd service

ExecStartPre  → just agents-sync      (reconcile fleet to SQLite agents table)
ExecStart     → bun run apps/server/src/main.ts
ExecStopPost  → just containers-down  (stop all agent containers)

agents-sync walks every row in the agents SQLite table and brings Docker in line: creates missing containers, removes orphans, recreates any whose image or credentials source drifted.

Creating a new agent instance

Creating a row from the dashboard triggers reconcileOne automatically — within seconds the container and state volume are created. If you bypass the dashboard (direct SQL insert, service down):

just agents-sync   # idempotent, safe to run while the service is up

Graceful shutdown

systemctl --user restart claude-hooks (or any SIGTERM / SIGINT) triggers the graceful-shutdown handler before systemd's grace timer expires:

  1. Closes Bun.serve — new requests are refused at the TCP layer.
  2. Waits up to shutdown.drain_ms (default 60 s) for in-flight tasks.
  3. On timeout, calls abort() on every still-busy worker and marks its task_history row cancelled with reason shutdown.
  4. For container workers, signals the in-container Claude CLI to stop.

Tune the drain budget via Settings → Service:

{ "shutdown": { "drain_ms": 120000 } }  // clamped to [1000, 270000] ms

TimeoutStopSec=300 gives the process up to five minutes before systemd force-kills it.

Manage

just install    # copy unit, daemon-reload, enable, start
just status     # systemctl --user status claude-hooks
just logs       # journalctl --user -u claude-hooks
just uninstall  # stop, disable, remove unit

Container management

just containers-up                 # start all agent containers
just containers-down               # stop all (volumes persist)
just containers-rebuild            # pull latest image, recreate all containers
just containers-rebuild dev        # recreate only the dev container
just containers-logs               # last 50 lines from every container
just containers-logs dev           # tail -f logs for dev
just containers-shell dev          # docker exec -it bash into dev

First-time container setup

  1. Provision per-agent credentials:

    mkdir -p ~/.config/claude-hooks/claude-credentials
    cp ~/.claude/.credentials.json \
        ~/.config/claude-hooks/claude-credentials/.credentials.json
    chmod 600 ~/.config/claude-hooks/claude-credentials/.credentials.json
    
  2. Build the image locally (if no published image yet):

    docker buildx build --platform linux/amd64 -t <registry>/<owner>/claude-hooks:latest .
    
  3. Create and start a container:

    just containers-rebuild reviewer
    
  4. Smoke-test:

    just containers-smoke reviewer
    
  5. Enable container mode for the agent type in Settings → Agent types → reviewer — flip agent_type_container.enabled to true.

  6. Restart the service.

Token rotation

echo '<new-token>' > ~/.config/claude-hooks/tokens/<name>
just containers-rebuild <name>   # only that container is recreated

Patched forgejo-mcp in the image

The image builds forgejo-mcp from source with local patches under patches/. They fix upstream bugs where merge_pull_request silently returns success on HTTP rejects and update_issue drops the assignee argument. The patched binary reports version ${FORGEJO_MCP_VERSION}+claude-hooks.<n>. Remove the patches once upstream ships the fixes.


Security

Claude-hooks trusts an external Authelia instance (or any proxy that sets Remote-User) rather than a homegrown token scheme.

  • Mutating endpoints (/task, /cancel, /breakdown, /agents POST/PATCH/DELETE, /architect/*) require an authenticated operator.
  • Read-only monitor endpoints (/health, /events, /queue, /history, /stats, /usage) remain open on the LAN.

Reverse proxy setup

Create a vhost in your proxy stack pointing at the service and pass requests through Authelia:

server {
    server_name claude.<your-domain>;

    location / {
        auth_request /authelia;
        auth_request_set $user $upstream_http_remote_user;
        proxy_set_header Remote-User $user;

        proxy_pass http://<host>:4500;
        proxy_set_header Host $host;
    }

    location = /authelia {
        internal;
        proxy_pass http://127.0.0.1:9091/api/verify;
    }
}

Authelia access control:

rules:
  - domain: claude.<your-domain>
    policy: one_factor
    subject: "user:<username>"

Service-side auth config

Edit Settings → Service or write a service_config row directly:

{
  "auth": {
    "operator_user": "<username>",
    // IPv4 CIDRs of the reverse proxy that sets Remote-User (must be non-empty)
    "trust_proxy": ["10.0.0.0/24"],
    "authelia_logout_url": "https://auth.<your-domain>/logout"
  }
}

Without authentication: omit the auth block to leave mutating routes open on the LAN. Add the block only after the vhost + Authelia rule are in place.

How Remote-User is trusted

Remote-User is a plain HTTP header — any LAN client could forge it. The server counters this by verifying the TCP connection source IP against trust_proxy before honouring the header. User-supplied x-claude-client-ip headers are stripped first so external callers cannot spoof a trusted IP.

Verify the chain:

curl http://localhost:4500/whoami
# → {"user":null,"auth_enabled":true,"logout_url":"https://auth.<your-domain>/logout"}

Debugging with auditd

When a claude-hooks-* container vanishes without a service-side reconcileOne call, the repo ships an auditd rule that logs every docker CLI invocation by uid 0 or uid 1000 with its PID, argv, exe, and cwd, tagged claude-hooks-docker.

Requires the audit package and a running auditd service.

just audit-install   # install rule (requires sudo)
just audit-tail      # follow new events in near real time
just audit-uninstall # remove rule and reload

One-shot inspection:

sudo ausearch -k claude-hooks-docker -ts today -i

API reference

Core task

Method Path Description
POST /task Submit a coding task (returns task_id)
GET /task/:id Get task result (success/failure/running/queued)
POST /task/:id/steer Mid-flight operator steering — inject a message into the running agent
POST /task/:id/redispatch Re-dispatch a failed or cancelled task
GET /health Service health + queue depth
GET /queue Current task + queued tasks per worker
POST /cancel Abort the running task
GET /history Recent task history (last 50, in-memory)
GET /storage Disk usage for cache clones / worktrees / sessions
GET /events SSE stream of real-time task events
POST /reset Drop worktree + session for a (agent, repo, issue) tuple
POST /sweep Run one pass of the expiration sweeper on demand

Agent management

Method Path Description
GET /agents List every SQLite row + type defaults + live worker state
POST /agents Create a new instance row + reconcile container
PATCH /agents/:name Update override fields + reconcile container
DELETE /agents/:name Cancel task, remove container, drop row
GET /agents/health Aggregate fleet saturation, queue depth, cost
GET /agents/:name/provider-events Failover ledger for one instance
POST /agents/:name/reset-tier Reset to tier 1, clear paused, clear failure metadata
POST /agents/:name/pause Pause the agent (no new tasks accepted)
POST /agents/:name/unpause Resume a paused agent
GET /agents/models Provider-supplied model catalogue (?provider=<id>)

Breakdown

Method Path Description
POST /breakdown Run the breakdown skill on a specs/*.md file

POST /breakdown dispatches the breakdown skill against a spec document (specs/<name>.md) in a watched repo. The code-lead pool turns each logical ## section into a type:user-story issue.

curl -X POST http://localhost:4500/breakdown \
  -H 'Content-Type: application/json' \
  -d '{"repo":"<owner>/<repo>","spec_path":"specs/my-feature.md","tracking_issue":47}'
Field Required Description
repo yes Must match a row in watched_repos
spec_path no Defaults to specs/ (walk all .md)
tracking_issue no Issue on which the skill posts its summary comment
milestone no Milestone title attached to every created issue
dry_run no true ⇒ post proposed issues as a checklist, don't create
max_issues no Per-dispatch cap; defaults to 15

Response: 202 { task_id, worker, repo, tracking_issue }.

The same guardrails apply to the webhook /breakdown slash command — posting a comment starting with /breakdown on any watched-repo issue dispatches the same skill. Trailing args: positional (/breakdown specs/foo.md) or key=value (/breakdown spec_path=specs/foo.md milestone="v1.0").

Stats and pipeline

Method Path Description
GET /stats Cost / turn / success-rate aggregates
GET /usage Token-consumption rollup against the weekly quota
GET /issues/pipeline Per-issue canonical stage model
GET /issues/ready Open issues with all blockers closed + no /hold
GET /issues/deps k-hop dependency DAG for one issue
POST /pipeline/bounce-review Rekick a stalled reviewer assignment

Watchdog

Method Path Description
GET /watchdog/status Dead-letter PRs and watchdog state
POST /watchdog/retry/:repo/:number Remove a dead-letter entry (auth-gated)
POST /watchdog/snooze/:repo/:number Snooze a dead-letter PR for 24 h (auth-gated)
GET /watchdog/recovery/diff Git diff for a stash branch (auth-gated)
DELETE /watchdog/recovery Force-delete stash branch (auth-gated)

Settings

Method Path Description
GET /settings Read service settings
PATCH /settings Update service settings
GET /whoami Inspect what the server sees for auth
GET /watched-repos List all watched repos
POST /watched-repos/:owner/:name/rotate-secret Rotate the webhook secret for a repo

Recipes reference

just dev                        # Run service with hot reload
just start                      # Run in production
just qa                         # Typecheck + lint + format check + tests
just agents-sync                # Reconcile per-agent containers vs. SQLite agents table
just containers-rebuild [NAME]  # Pull/build image and recreate containers (one or all)
just deps-backfill REPO [TOKEN] # Promote prose-only issue dependencies to native Forge API
just redispatch-interrupted     # Re-queue tasks stuck in 'running' after an unclean restart
just flows-apply FILE           # Apply a flow-graph definition file to the service
just render-agent-env [NAME]    # Print the resolved env for one (or all) agent instances
just oauth-register-forgejo     # Register OAuth app credentials for a Forgejo instance
just oauth-register-github      # Register OAuth app credentials for GitHub
just audit-install              # Install auditd rules that log docker stop/rm on containers
just audit-tail                 # Follow new events tagged claude-hooks-docker
just audit-uninstall            # Remove the auditd rules and reload

Migration note

If you have an existing installation using the on-host cache (pre-containerisation), that state is not migrated into Docker volumes. Volumes start empty and are populated as agents pick up new tasks.

systemctl --user stop claude-hooks
rm -rf ~/.cache/claude-hooks          # abandon old state
just containers-rebuild               # create containers (image + tokens + credentials)
just install                          # install updated unit

Release pipeline

Tag pushes (v*) fire .forgejo/workflows/release.yml. The pipeline:

  1. Builds both arch binaries and attaches them to a Forgejo release.
  2. Builds + pushes a multi-arch container image.
  3. Runs runtime smoke tests (bun, git, forgejo-mcp, claude --version) plus a credential-file audit against the just-pushed image.

On failure the image tags are deleted from the registry and the release is flipped to prerelease with a "DO NOT USE" banner.

The publish-image job requires Docker daemon access and is gated on a dedicated release-only runner label (runs-on: [docker, release]). PR CI (qa.yml) stays on plain docker and cannot land on the socket-mounted runner. See docs/runner-setup.md for runner registration, config.yml snippets, and the release-candidate dry-run procedure.


Further reading

Doc Topic
docs/api.md Complete HTTP route reference
docs/agents-architecture.md Agent types vs. instances
docs/architect.md Host-mode architect — gate, dispatch, security rails
docs/containers.md Container reconciliation + watchdog
docs/credentials.md Credentials mount + agent-env-sync
docs/database.md Drizzle + bun:sqlite, migration runner
docs/label-routing.md Label routing + pipeline stall thresholds
docs/design-review.md Design review flow detail
docs/providers.md AI provider failover + routing
docs/multi-repo.md Multi-repo dispatch gate + bootstrap
docs/multi-forge.md Multi-forge (Forgejo / GitHub / GitLab)
docs/configuration.md Service configuration reference
docs/breakdown.md Breakdown skill detail
docs/dependencies.md Issue dependency propagation
docs/reviewer-instances.md Specialised reviewer pool
docs/plugins.md Per-agent Claude Code plugins
docs/penpot.md Penpot MCP integration
docs/forge-mcp.md Forge MCP (agent-side)
docs/workspace.md Workspace + web app detail
docs/shutdown.md Graceful shutdown
docs/i18n.md i18n (Paraglide)
docs/modules.md Every file in apps/server/src/