Clayde is a persistent autonomous AI software agent that lives on a dedicated VM and works GitHub issues end-to-end — researching codebases, writing plans, implementing solutions, and opening pull requests.
Clayde is assigned GitHub issues in software repositories. For each issue it:
- Checks for new whitelist-visible activity since its last access
- Invokes Claude with the full issue context — Claude decides what to do next: ask clarifying questions, post a plan, implement the solution, or address review comments
- Posts a summary comment after each work cycle
- Opens a pull request (Claude creates the PR directly with a description and, for diffs spanning more than 3 files, a recommended reading order) and assigns the issue author as reviewer
- Monitors the PR and addresses review comments when they appear
Clayde runs as a Docker container in a continuous loop (default: every 5 minutes). Rather than a rigid state machine, it uses timestamp-based activity detection: each issue records the last time it was processed, and only new visible activity since that timestamp triggers a new Claude invocation.
Clayde's loop is event-driven and stateless by design:
- Fetch assigned issues from GitHub.
- For each issue: check whether there is new whitelist-visible activity (comments or PR reviews) since
last_seen_at. If the issue has never been seen, or a previous run was interrupted, it is always processed. - Invoke Claude once with the full context: issue body, all visible comments, and any open PR reviews. Claude decides the next action — no hard phases.
- Detect PR: after each run, check for an open PR on the working branch and persist its URL.
- Update
last_seen_atto the current time so Clayde's own reply comments don't re-trigger a cycle. - Crash recovery:
in_progressis set before invoking Claude and cleared after. If the process crashes mid-run, the next cycle retries automatically. - Pure PR approvals (no comments) update
last_seen_atwithout invoking Claude. - Closed issues are pruned from state automatically.
Clayde uses content filtering rather than gatekeeping which issues to work on. It will only act on content that is visible:
- An issue body or comment is visible if it was written by a whitelisted user, or has a 👍 reaction from a whitelisted user.
- If an issue has no visible content at all, it is skipped.
- Blocked issues (those with "blocked by #N" or "depends on #N" in the body) are also skipped.
There are no hard approval gates — Claude engages with the issue as soon as there is visible content, and the human can steer the conversation by replying in the issue thread.
Whitelisted users are configured via CLAYDE_WHITELISTED_USERS in data/config.env.
- Multi-repo support: Clones and works on any GitHub repository it has access to
- Event-driven loop: Only invokes Claude when there is new visible activity — no wasted cycles
- Natural conversation: Claude engages directly in the issue comment thread, asking questions and posting plans as needed
- Full issue lifecycle: Engage → implement → PR → review, all driven by new activity
- PR creation by Claude: Claude writes the PR description and a recommended reading order for larger diffs
- PR review handling: Reads and addresses reviewer feedback automatically
- Rate-limit resilience: Detects Claude usage limits and automatically retries
- Crash recovery:
in_progressflag ensures interrupted runs are retried next cycle - Safety filtering: Whitelist-based content filtering prevents acting on unauthorized content
- Observability: OpenTelemetry tracing with JSONL file export
- Dual Claude backend: Use the Anthropic API (pay-per-token) or the Claude Code CLI (subscription-based)
| Component | Tool |
|---|---|
| Language | Python 3.13 |
| Package manager | uv |
| LLM | Claude (Anthropic SDK or Claude Code CLI) |
| GitHub API | PyGitHub |
| Deployment | Docker (continuous loop) |
| Configuration | pydantic-settings |
| Templating | Jinja2 |
| Observability | OpenTelemetry |
| State persistence | state.json |
Create a GitHub account for your bot (e.g. my-bot). This is the account that will be assigned issues and open pull requests.
From the bot account, create a classic personal access token with the full repo scope.
mkdir -p data/logs data/repos
cp config.env.template data/config.envEdit data/config.env:
CLAYDE_GITHUB_TOKEN=github_pat_...
CLAYDE_GITHUB_USERNAME=my-bot
CLAYDE_GIT_EMAIL=my-bot@example.com
CLAYDE_ENABLED=true
CLAYDE_WHITELISTED_USERS=your-username,my-bot
See Configuration for all available settings.
Clayde supports two backends for invoking Claude, selected by CLAYDE_CLAUDE_BACKEND in data/config.env:
Uses the Anthropic Python SDK with a tool-use loop. Pay-per-token.
- Get an API key from console.anthropic.com
- Set in
data/config.env:CLAYDE_CLAUDE_BACKEND=api CLAYDE_CLAUDE_API_KEY=sk-ant-...
Runs the Claude Code CLI as a subprocess. Uses your Claude Pro/Max subscription — no per-token cost.
- On the host machine, log in to the CLI:
claude login
- Set in
data/config.env:(CLAYDE_CLAUDE_BACKEND=cliCLAYDE_CLAUDE_API_KEYis not required for the CLI backend.)
The docker-compose.yml mounts ~/.claude/.credentials.json from the host directly into the container. Token refreshes, logouts, and account switches on the host are immediately reflected.
docker compose up -dClayde will start its loop, checking for assigned issues every 5 minutes (configurable via CLAYDE_INTERVAL).
In any repository the bot has access to, assign issues to the bot account. Clayde will pick them up automatically on the next loop cycle.
data/config.env (plain KEY=VALUE, all prefixed with CLAYDE_):
| Key | Purpose |
|---|---|
CLAYDE_GITHUB_TOKEN |
Classic PAT with full repo scope |
CLAYDE_GITHUB_USERNAME |
The bot account username |
CLAYDE_GIT_NAME |
Git commit author name (defaults to CLAYDE_GITHUB_USERNAME if not set) |
CLAYDE_GIT_EMAIL |
Git commit author email (required) |
CLAYDE_ENABLED |
Set to true to activate |
CLAYDE_WHITELISTED_USERS |
Comma-separated trusted GitHub usernames |
CLAYDE_INTERVAL |
Loop interval in seconds (default: 300) |
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 300) |
CLAYDE_PEBBLE_QUEUE_MAX |
Max queued jobs before 503 (default 100) |
CLAYDE_NTFY_TOPIC |
ntfy.sh topic for Pebble outcome notifications |
CLAYDE_NTFY_BASE_URL |
ntfy base URL (override for self-host) |
CLAYDE_NTFY_TIMEOUT_S |
ntfy POST timeout seconds (default 10) |
CLAYDE_KB_PATH |
In-container KB path; Pebble per-request cwd (default /home/clayde/knowledge_base) |
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:
- Set
CLAYDE_PEBBLE_ENABLED=trueand a strong randomCLAYDE_PEBBLE_TOKENindata/config.env. - Set
CLAYDE_PEBBLE_HOSTto the public hostname Traefik should serve (e.g.clayde.example.com). The hostname must resolve to the host's public IP and ports80+443must be open for Let's Encrypt HTTP-01 challenges. - Mount one or more skill directories under
/skills/indocker-compose.yml. Each skill is a markdown file with frontmatternameanddescription(seeCLAUDE.mdfor the full format). Built-in skills (currentlyping) are baked into the image at/skills/builtin/. - Mount
~/knowledge_baseto/home/clayde/knowledge_base(already wired indocker-compose.yml) so Claude has a writable working directory. Sync across devices is handled by Syncthing on the host — the container performs nogitagainst the KB. - Set
CLAYDE_NTFY_TOPIC(and optionallyCLAYDE_NTFY_BASE_URLfor self-hosted ntfy) to receive outcome notifications on your phone for every Pebble request. - Configure the Pebble app to POST to
https://<CLAYDE_PEBBLE_HOST>/webhook/pebblewith 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)
with cwd set to the knowledge-base mount. Claude is free to use any
number of skills per request; every terminal outcome (success, failure,
timeout, usage limit, queue full, etc.) emits an ntfy notification.
