Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ad4c64b
Add design doc for Pebble webhook + skill framework
ClaydeCode May 6, 2026
a00c2e5
Align compose volume mount example with skill dirs env var example
ClaydeCode May 6, 2026
08124e0
Address spec review notes: fixed /skills/ path, single-skill prompt r…
ClaydeCode May 7, 2026
ab5be0d
Add implementation plan for Pebble webhook + skill framework
ClaydeCode May 7, 2026
cbccdd5
feat(pebble): add settings fields and webhook dependencies
May 7, 2026
963f2c2
feat(pebble): add Skill model and frontmatter parser
May 7, 2026
d397ce5
fix(pebble): reject whitespace-only name/description in skill frontma…
May 7, 2026
0842dd9
feat(pebble): recursive skill discovery with dedup and skip-on-error
May 7, 2026
207422e
feat(pebble): build system + user prompts for CLI invocation
May 7, 2026
6c812a9
feat(pebble): bearer-token verification with constant-time compare
May 7, 2026
c1aafc3
feat(pebble): in-memory asyncio job queue with non-blocking enqueue
May 7, 2026
e2211fb
feat(pebble): async Claude CLI runner with fresh session per call
May 7, 2026
74c0efd
fix(pebble): kill subprocess on caller cancellation; add branch tests
May 7, 2026
cd33736
feat(pebble): worker loop with OTel process span
May 7, 2026
ba68698
feat(pebble): FastAPI app with bearer auth, queue, and OTel enqueue span
May 7, 2026
8d46c64
feat(pebble): async orchestrator entry that gates webhook on pebble_e…
May 7, 2026
a8a8265
feat(pebble): docker-compose with Traefik, private network, /skills m…
ClaydeCode May 7, 2026
d19d95c
docs(pebble): document webhook endpoint, skill format, and operator s…
ClaydeCode May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ src/clayde/
plan.py # run_preliminary(url), run_thorough(url), run_update(url, phase)
implement.py # run(issue_url) — implement + open PR + assign reviewer
review.py # run(issue_url) — address PR review comments
webhook/
__init__.py
app.py # FastAPI app, /webhook/pebble, /health, OTel enqueue span
auth.py # constant-time bearer-token verification
queue.py # PebbleJob, JobQueue (in-memory asyncio.Queue), QueueFullError
runner.py # invoke_claude_pebble — async CLI subprocess, fresh session
skills.py # Skill model, /skills/ discovery, system + user prompt builders
worker.py # worker_loop, process_job — pop jobs, OTel process span

# Container paths
/opt/clayde/ # application code (WORKDIR)
Expand Down Expand Up @@ -103,6 +111,12 @@ Plain `KEY=VALUE` file (no shell quoting). All keys use `CLAYDE_` prefix and are
| `CLAYDE_CLAUDE_API_KEY` | Anthropic API key for Claude SDK calls (required when backend=`api`) |
| `CLAYDE_CLAUDE_MODEL` | Model to use (default: `claude-opus-4-6`) |
| `CLAYDE_CLAUDE_BACKEND` | `api` (default) or `cli` — selects Anthropic SDK or Claude Code CLI |
| `CLAYDE_PEBBLE_ENABLED` | Set to `true` to enable the Pebble webhook |
| `CLAYDE_PEBBLE_TOKEN` | Bearer token the Pebble app sends |
| `CLAYDE_PEBBLE_HOST` | Public hostname for Traefik routing |
| `CLAYDE_PEBBLE_PORT` | Internal HTTP port (default 8080) |
| `CLAYDE_PEBBLE_TIMEOUT` | Per-request CLI timeout seconds (default 600) |
| `CLAYDE_PEBBLE_QUEUE_MAX` | Max queued jobs before 503 (default 100) |

Config is loaded via `get_settings()` (singleton). `GH_TOKEN` is exported at startup for the `gh` CLI.

Expand Down Expand Up @@ -287,6 +301,45 @@ Logger names: `clayde.orchestrator`, `clayde.tasks.plan`, `clayde.tasks.implemen

---

## Pebble Webhook

When `CLAYDE_PEBBLE_ENABLED=true`, the container also serves a FastAPI
webhook for a Pebble watch app, alongside the existing GitHub poll loop
(both run on the same asyncio event loop).

- `POST /webhook/pebble` — accepts `{"text": str, "timestamp": int}` with
`Authorization: Bearer <CLAYDE_PEBBLE_TOKEN>`. Returns 200 with a job id.
- `GET /health` — liveness probe (no auth).

The text is dispatched to the Claude CLI with a system prompt listing
*skills* found under the in-container path `/skills/`. Each skill is a
single markdown file with frontmatter:

```markdown
---
name: my-skill
description: One-line description used in skill catalog.
---

(Body: instructions for Claude.)
```

Mount one or more host directories read-only under `/skills/` in
`docker-compose.yml`. Discovery is recursive; subdirectory layout is
free. Duplicate `name` fields are logged and only the first-discovered
skill is used.

Claude must pick AT MOST ONE skill per request, or respond exactly
"No matching skill". Each request gets a fresh `claude` session — no
context carries between requests.

Traefik handles TLS (Let's Encrypt) and routes
`https://<CLAYDE_PEBBLE_HOST>/webhook/pebble` over a private docker
network. The `clayde` service is not attached to any externally-reachable
network — the only ingress path is through Traefik.

---

## Testing

Run the test suite after any feature development or bug fix:
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,37 @@ In any repository the bot has access to, assign issues to the bot account. Clayd
| `CLAYDE_CLAUDE_BACKEND` | `api` (default) or `cli` |
| `CLAYDE_CLAUDE_API_KEY` | Anthropic API key (required when backend=`api`) |
| `CLAYDE_CLAUDE_MODEL` | Model to use (default: `claude-opus-4-6`) |
| `CLAYDE_PEBBLE_ENABLED` | Set to `true` to enable the Pebble webhook |
| `CLAYDE_PEBBLE_TOKEN` | Bearer token the Pebble app sends |
| `CLAYDE_PEBBLE_HOST` | Public hostname for Traefik routing |
| `CLAYDE_PEBBLE_PORT` | Internal HTTP port (default `8080`) |
| `CLAYDE_PEBBLE_TIMEOUT` | Per-request CLI timeout seconds (default `600`) |
| `CLAYDE_PEBBLE_QUEUE_MAX` | Max queued jobs before 503 (default `100`) |

---

## Pebble Watch Integration

Clayde can also receive voice commands from a Pebble watch app via an
HTTPS webhook. When enabled, the container additionally serves a FastAPI
endpoint alongside the existing GitHub poll loop.

To enable:

1. Set `CLAYDE_PEBBLE_ENABLED=true` and a strong random
`CLAYDE_PEBBLE_TOKEN` in `data/config.env`.
2. Set `CLAYDE_PEBBLE_HOST` to the public hostname Traefik should serve
(e.g. `clayde.example.com`). The hostname must resolve to the host's
public IP and ports `80` + `443` must be open for Let's Encrypt
HTTP-01 challenges.
3. Mount one or more skill directories under `/skills/` in
`docker-compose.yml`. Each skill is a markdown file with frontmatter
`name` and `description` (see `CLAUDE.md` for the full format).
4. Configure the Pebble app to POST to
`https://<CLAYDE_PEBBLE_HOST>/webhook/pebble` with the bearer token.

The webhook is fire-and-forget: requests return `200` with a job id and
work happens asynchronously in a single serial worker. Each request
spawns a fresh Claude CLI session (no context carries between requests)
and the system prompt instructs Claude to choose at most one matching
skill or respond `"No matching skill"`.
16 changes: 16 additions & 0 deletions config.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,19 @@ CLAYDE_WHITELISTED_USERS=your-username,your-bot-username
# "cli" (Claude Code CLI, requires OAuth credentials mounted)
CLAYDE_CLAUDE_BACKEND=api
CLAYDE_CLAUDE_API_KEY=

# --- Pebble webhook ---
# Set to true to enable the FastAPI webhook on port 8080 (routed via Traefik).
CLAYDE_PEBBLE_ENABLED=false
# Bearer token the Pebble app sends in Authorization: Bearer <token>.
# Generate a long random string and configure it in the Pebble app's settings.
CLAYDE_PEBBLE_TOKEN=
# Public hostname for Traefik routing (e.g. clayde.example.com).
# Required when CLAYDE_PEBBLE_ENABLED=true.
CLAYDE_PEBBLE_HOST=
# Internal HTTP port (default 8080; Traefik backend target).
CLAYDE_PEBBLE_PORT=8080
# Per-request CLI timeout in seconds.
CLAYDE_PEBBLE_TIMEOUT=600
# Maximum queued Pebble jobs before 503.
CLAYDE_PEBBLE_QUEUE_MAX=100
39 changes: 39 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
networks:
web:
internal:

services:
traefik:
image: traefik:v3
restart: unless-stopped
networks: [web, internal]
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=internal
- --entrypoints.websecure.address=:443
- --entrypoints.web.address=:80
- --certificatesresolvers.le.acme.email=${CLAYDE_GIT_EMAIL}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/letsencrypt:/letsencrypt
labels:
- "com.centurylinklabs.watchtower.enable=true"

clayde:
image: ghcr.io/claydecode/me:main
restart: unless-stopped
user: "1000:1000"
networks: [internal]
expose:
- "8080"
environment:
- CLAYDE_ENABLED=true
volumes:
- ./data:/data
# Mount Claude CLI OAuth credentials (required when CLAYDE_CLAUDE_BACKEND=cli)
- ~/.claude/.credentials.json:/home/clayde/.claude/.credentials.json
# Pebble skill directories — mount one or more host dirs read-only
# under /skills/. Subdirectory layout is free; discovery is recursive.
- ~/skills/personal:/skills/personal:ro
- ~/skills/shared:/skills/shared:ro
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.enable=true"
- "traefik.http.routers.clayde.rule=Host(`${CLAYDE_PEBBLE_HOST}`) && PathPrefix(`/webhook`)"
- "traefik.http.routers.clayde.entrypoints=websecure"
- "traefik.http.routers.clayde.tls.certresolver=le"
- "traefik.http.services.clayde.loadbalancer.server.port=8080"

watchtower:
image: containrrr/watchtower
Expand Down
Loading
Loading