Critters is a TypeScript daemon that polls issue trackers (Linear, Jira, and GitHub Issues) for labeled issues and runs configurable agentic workflows on them using coding CLIs (Claude Code and/or Codex) — planning, execution, review, or fully custom phases. It runs on Bun and orchestrates everything through tmux panes.
Inspired by Stripe's Minions.
curl -fsSL https://raw.githubusercontent.com/ack-ventures/critters/main/install.sh | bashThis downloads the latest binary, installs it to your PATH, and walks you through initial setup.
critters— start the daemoncritters version— show versioncritters update— check for and apply updatescritters stop— stop the daemon gracefullycritters init— (re-)configure~/.critters/critters status— show daemon status (active/queued critters, today's stats)critters retry <ID>— retry a failed critter (reset to Todo)critters logs <ID>— show logs for a critter run (--phase,--follow)critters tail— live-stream output from all active critters (--type)critters history— show past critter runs (--last N,--failed,--type,--json)critters kill <ID>— kill a running critter (--all,--type)critters kickoff— trigger an immediate poll (instead of waiting for the next interval)critters list-types— show configured critter typescritters init-repo— scaffold.critters.yamlin the current repocritters prompt-help— launch Claude to help design critter types and promptscritters prompt render <type> <phase>— preview a phase prompt with variables substituted, skills concatenated, and tools resolved (flags:--identifier,--title,--description,--repo,--branch,--work-dir,--json)critters clean— clean up stale work directories (--all,--dry-run,--branches,--panes)critters release-notes— show release notes for recent versionscritters validate— validate config file without starting daemoncritters help— show usage
--dry-run— poll once, show what would happen, and exit--no-tmux— daemonize to background without tmux (logs to~/.critters/critters.log, writes PID to~/.critters/critters.pid)--no-watch— disable config file watching (no hot-reload)--skip-update— skip auto-update check on startup--config PATH— use a custom config file--type NAME— filter dry-run to a specific critter type--json-logs— output structured JSON logs (one object per line)
See docs/self-hosting.md for the full deployment guide.
Quick start (binary):
curl -fsSL https://raw.githubusercontent.com/ack-ventures/critters/main/install.sh | bash
critters init
tmux new -s critters
crittersPrerequisites: Bun, Claude Code CLI (authenticated) and/or Codex, gh CLI (authenticated), tmux, jq
# Clone and install
git clone https://github.com/ack-ventures/critters && cd critters
bun install
# Configure
cp .env.example .env
# Edit .env and set LINEAR_API_KEY (for Linear) and/or JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN (for Jira)
# and/or GITHUB_TOKEN (for GitHub Issues)
# Optionally set SLACK_WEBHOOK_URL for notifications, or SLACK_BOT_TOKEN + SLACK_CHANNEL for threaded notifications
# Optionally tweak critters.config.yaml
# Run
bun start
# or: bun run src/index.ts# Clone and configure
git clone https://github.com/ack-ventures/critters && cd critters
cp .env.example .env
# Edit .env — set ANTHROPIC_API_KEY and LINEAR_API_KEY (and/or JIRA_* vars)
# Start
docker compose up -d
# View logs
docker compose logs -f
# Dashboard at http://localhost:3847The Docker image includes all runtime dependencies (Claude Code CLI, Codex CLI, gh, git, jq).
Installing additional tools (Node.js, Bun, Python, etc.):
The container doesn't include language runtimes by default. To install tools at startup without rebuilding the image, create a setup script at ~/.critters/setup.sh (automatically mounted into the container):
#!/bin/bash
# ~/.critters/setup.sh — runs before the daemon starts
# Node.js (via nvm)
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
nvm install 22
# Bun
curl -fsSL https://bun.sh/install | bash
# Python
apt-get update && apt-get install -y --no-install-recommends python3 python3-pipThe script is source'd so any PATH or env changes persist into the daemon and spawned Claude Code sessions. Override the path with CRITTERS_SETUP_SCRIPT env var.
Auth requirements:
CLAUDE_CODE_OAUTH_TOKEN— Claude Max/Team subscription token (runclaude setup-tokenon the host, then add to.envor export in your shell). Alternatively, setANTHROPIC_API_KEYfor direct API access.~/.codex— Codex CLI auth/config (mounted into the container when using Codex phases)~/.ssh— SSH keys for git clone (mounted read-only)GITHUB_TOKEN— GitHub CLI auth (rungh auth tokenon the host, then add to.envor export in your shell)
For the pre-built image, replace build: . with image: ghcr.io/ack-ventures/critters:latest in docker-compose.yaml.
To use the latest released binary instead of building from source, set the build target to prod:
# docker-compose.yaml
services:
critters:
build:
context: .
target: prod # downloads released binary instead of building from sourceOr build directly: docker build --target prod . (append --build-arg CRITTERS_VERSION=1.6.1 to pin a version).
- Watcher polls your issue tracker (Linear, Jira, and/or GitHub Issues) every 120 seconds for issues with the "Critter" label in "Todo" status.
- Spawner shallow-clones the target repo into a temp directory and creates a feature branch.
- Phase 1 (Planning): A configured coding CLI instance explores the codebase and writes an implementation plan.
- Phase 2 (Execution): A configured coding CLI instance implements the plan, commits, pushes, and opens a draft PR.
- Status updates flow back to the issue tracker: Todo → In Progress → In Review (on PR) or Critter Failed (on error).
The built-in critter types form a pipeline:
Todo → In Progress → In Review → review ──┬──► Done (merged)
(create) │
┌──────────────────┘
▼
Human Review
│
└──► fix-review-comments ──► In Review (loops back to review)
- create picks up "Todo" issues, plans and implements the work, opens a draft PR, and moves the issue to "In Review".
- review picks up "In Review" issues, reviews the PR diff, and either merges it (→ "Done") or requests changes (→ "Human Review").
- fix-review-comments (optional, user-configured) picks up "Human Review" issues, reads the review feedback from the PR, addresses it, and moves the issue back to "In Review" — sending it through review again.
Without fix-review-comments, issues that need changes stop at "Human Review" for manual intervention (or critters retry --force).
To enable the loop, add a fix-review-comments type to your config:
critterTypes:
# ... create and review types ...
fix-review-comments:
trigger: { label: "Critter Review", status: "Human Review" }
repo: { clone: true, branch: true }
phases:
- name: fix
prompt: ~/.critters/prompts/fix-review.md
model: opus
maxTurns: 50
tools: default
outcomes:
success: { status: "In Review" }
failure: { status: "Critter Failed" }
concurrency: 2
timeoutMinutes: 20
enrichment: extractPrUrl
claimStatus: "In Progress"The prompt file should instruct Claude to read the PR review feedback (for example with gh pr view --comments plus gh api repos/:owner/:repo/pulls/:number/comments for inline review comments), make the requested fixes, commit, and push. Use builtin:execution prompts as a starting point. claimStatus prevents duplicate dispatch if two poll cycles trigger before the fix completes.
Settings live in critters.config.yaml:
| Field | Default | Description |
|---|---|---|
provider |
"linear" | Default issue tracker: "linear", "jira", or "github" |
pollIntervalSeconds |
120 | How often to poll for issues |
concurrency |
2 | Max parallel critters |
timeoutMinutes |
30 | Total timeout per task (both phases) |
workDir |
/tmp/critters-work | Temp clone directory |
triggerLabel |
"Critter" | Label that triggers pickup |
maxPlanningTurns |
50 | Max turns for planning phase |
maxExecutionTurns |
75 | Max turns for execution phase |
defaultAllowedTools |
see file | Tools critters can use |
repos |
{} | Project ID → repo URL + extra tools |
teamRepos |
{} | Team ID → fallback repo URL |
defaultRepo |
— | Final fallback repo URL when not in description, project config, or team config |
tmuxSession |
"critters" | Name of the tmux session to use |
branchPrefix |
"critter" | Prefix for feature branch names (<prefix>/<ID>-<slug>) |
planningModel |
"opus" | Model for planning phase |
executionModel |
"opus" | Model for execution phase |
healthPort |
3847 | HTTP server port for dashboard and health checks (0 to disable) |
dashboardToken |
— | Shared secret for dashboard POST endpoints (also reads DASHBOARD_TOKEN env var) |
maxLogSizeMb |
10 | Max log file size in MB before rotation (with --no-tmux) |
cleanupIntervalMinutes |
60 | How often periodic work directory cleanup runs (minutes) |
cleanupStaleMinutes |
timeout + 30 | Age threshold (minutes) for considering work directories stale |
minDiskSpaceMb |
1024 | Minimum free disk space (MB) required before cloning a repo |
jiraStatusMap |
{} | Map critter status names to Jira workflow status names |
github |
— | GitHub Issues settings block: repos (list of "owner/repo"), statusField (default "Status"), statusMap, statusTypes — see "GitHub Issues as a ticket source" |
hooks |
{} | Shell commands run on lifecycle events (see below) |
costAlertThreshold |
— | Cost (USD) per task that triggers a Slack alert |
costBudget |
— | Cost (USD) per task that triggers a kill |
reviewTriggerLabel |
"Critter Review" | Label that triggers review pickup |
reviewModel |
"opus" | Model for reviews |
reviewConcurrency |
2 | Max parallel review critters |
reviewTimeoutMinutes |
15 | Timeout per review |
maxReviewTurns |
30 | Max turns per review |
cli |
"claude" | Default CLI adapter: "claude" or "codex" |
phases[].sandbox |
— | Optional phase-level sandbox override for adapters that support it, e.g. Codex read-only, workspace-write, or danger-full-access |
phases[].permissionMode |
— | Optional CLI approval override. Claude forwards this as --permission-mode (default, auto, acceptEdits, bypassPermissions, plan, dontAsk). Codex supports auto (--full-auto), bypassPermissions (--dangerously-bypass-approvals-and-sandbox), default, and dontAsk. |
autoRetry |
— | Auto-retry config: { maxRetries, baseDelaySeconds, maxDelaySeconds } |
circuitBreaker |
— | Circuit breaker config: { failureThreshold, maxBackoffMinutes } |
mcpConfig |
— | Path(s) to MCP config JSON file(s), applied to all critters |
strictMcpConfig |
false | When true, passes --strict-mcp-config to prevent inheriting operator's MCP servers |
metricsRetentionDays |
90 | Days to retain metrics data before pruning |
autoUpdate |
— | Auto-update config: { enabled (default: true), intervalMinutes (default: 1440) } |
tunnel |
— | Tunnel config: { enabled, auth, domain } for ngrok remote access |
linearWebhookSecret |
— | Linear webhook signing secret (env: LINEAR_WEBHOOK_SECRET) |
jiraWebhookSecret |
— | Jira webhook secret (env: JIRA_WEBHOOK_SECRET) |
githubWebhookSecret |
— | GitHub webhook secret (env: GITHUB_WEBHOOK_SECRET) |
Per-repo tool overrides merge with the defaults:
repos:
"project-uuid":
url: "[email protected]:org/repo.git"
extraAllowedTools:
- "Bash(python:*)"
- "Bash(pip:*)"You can choose the CLI globally, per critter type, or per phase:
cli: claude
critterTypes:
create:
phases:
- name: planning
prompt: builtin:planning
cli: codex
model: gpt-5.2
maxTurns: 50
tools: readonly
- name: execution
prompt: builtin:execution
cli: claude
model: opus
maxTurns: 75
tools: defaultCritters preserves the existing tools config for both adapters. Claude enforces the allowlist directly; Codex uses sandboxing plus prompt-level restrictions, so enforcement is capability-based rather than identical.
For Codex phases that need outbound GitHub access through gh, set a phase sandbox explicitly. In practice, PR review and review-fixing phases often need danger-full-access, because Codex workspace-write can block the GitHub API calls that gh relies on.
Phases can also set a permissionMode to control CLI approval behavior. For Claude phases, auto is forwarded directly as --permission-mode auto:
critterTypes:
create:
phases:
- name: execution
prompt: builtin:execution
model: opus
maxTurns: 75
tools: default
permissionMode: autoThis lets Claude's auto-classifier approve reasonable tool calls outside the explicit allowlist.
For Codex phases, permissionMode: auto emits Codex's native --full-auto flag while still preserving the configured sandbox:
critterTypes:
create:
phases:
- name: planning
prompt: builtin:planning
cli: codex
model: gpt-5.5
maxTurns: 50
tools: readonly
permissionMode: autoIf a daemon runs inside an external container or VM sandbox where Codex's own sandbox cannot create namespaces, set permissionMode: bypassPermissions on that Codex phase. Critters emits --dangerously-bypass-approvals-and-sandbox for that mode, so only use it when the surrounding runtime already provides the execution boundary.
Shell commands that run on lifecycle events. Environment variables available: CRITTER_ISSUE_ID, CRITTER_IDENTIFIER, CRITTER_TITLE, CRITTER_REPO_URL, CRITTER_BRANCH, CRITTER_PR_URL (when applicable).
hooks:
onTaskStarted: "echo 'Task started'"
onPrCreated: "curl -X POST https://example.com/notify"
onMerged: "./scripts/post-merge.sh"
onTaskFailed: ""
onReviewStarted: ""
onNeedsChanges: ""The daemon runs an HTTP server on port 3847 (configurable via healthPort, set to 0 to disable).
| Route | Method | Description |
|---|---|---|
/ or /dashboard |
GET | Live dashboard (Console view) with task stats, charts, and recent activity. Includes a "New Critter" button for creating tickets |
/dashboard/inflight |
GET | Card grid of in-flight critters with elapsed/cost/timeout metrics |
/dashboard/queue |
GET | Queued critters with next-poll countdown and "Poll now" trigger |
/dashboard/history |
GET | Full activity table with type/status/repo filters and search |
/dashboard/logs |
GET | Split view: run list on the left, per-run detail (issue info, cost summary, phase timeline, logs) on the right |
/dashboard/types |
GET | Configured critter types with phase pipeline visualization and per-type run/success/cost stats |
/dashboard/repos |
GET | Configured repos with 14-day runs, success rate, cost, and extra tools |
/dashboard/hooks |
GET | Lifecycle hook commands + inbound webhook status + tunnel URL |
/dashboard/tokens |
GET | Presence-only listing of known env vars (values are never exposed) |
/dashboard/costs |
GET | Spend summary, breakdowns by type and repo, top 10 most expensive runs |
/dashboard/models |
GET | Models in use by phase, pulled from critter-type config |
/dashboard/releases |
GET | Release notes for recent versions, with the currently running version highlighted |
/dashboard/<id> |
GET | Deep link to a single critter's log page |
/healthz |
GET | JSON health check (uptime, version, per-type active/queued counts, active critter details, metrics summary) |
/metrics |
GET | JSON array of recent metric events |
/poll |
POST | Trigger an immediate poll cycle |
/review-poll |
POST | Trigger an immediate review poll cycle |
/restart |
POST | Restart the daemon process |
/stop |
POST | Stop the daemon gracefully |
/kill |
POST | Kill active critters by identifier ({ identifiers: string[] }) |
/api/v1/dashboard |
GET | Dashboard data payload (status, metrics, activity) |
/api/v1/issue/<id> |
GET | Per-issue data (phases, cost, PR status) for the log page |
/api/v1/metadata |
GET | Provider teams, critter types, and repo info |
/api/v1/issues |
POST | Create a critter issue ({ provider, teamId, title, description, critterType }) |
/api/v1/types |
GET | Configured critter types with per-type 14-day stats |
/api/v1/repos |
GET | Configured repos with per-repo 14-day stats |
/api/v1/models |
GET | Model usage pulled from phase configs |
/api/v1/hooks |
GET | Lifecycle hook commands + webhook + tunnel status |
/api/v1/env-status |
GET | Presence-only status of known env vars (no values) |
/api/v1/releases |
GET | Bundled release notes JSON |
/api/logs/<id> |
GET | Processed log tail for a critter run |
/api/logs/<id>/stream |
GET | SSE stream of a critter's live log |
/webhook/linear |
POST | Linear webhook endpoint (requires linearWebhookSecret) |
/webhook/jira |
POST | Jira webhook endpoint (requires jiraWebhookSecret) |
/webhook/github |
POST | GitHub webhook endpoint (requires githubWebhookSecret) |
When dashboardToken is configured, POST endpoints require a bearer token. critters status queries the health endpoint to display a quick summary in the terminal.
Webhook endpoints provide near-instant issue pickup instead of waiting for the next poll. Webhooks are additive — polling continues as the fallback. Set LINEAR_WEBHOOK_SECRET, JIRA_WEBHOOK_SECRET, and/or GITHUB_WEBHOOK_SECRET to enable. The daemon must be reachable from the internet — use the tunnel config for ngrok or a reverse proxy.
Works with both Linear and Jira. For a critter to pick up an issue, it needs:
- Label: "Critter" (exact match, configurable)
- Status: "Todo" (Linear), the mapped status via
jiraStatusMap(Jira), or the mapped Status field option /status:Todolabel (GitHub, see below) - Description: must include
repo: [email protected]:org/repo.giton its own line (unless a project or team mapping exists in the config)
Optionally, assign the issue to the relevant project and include implementation guidance in the description -- the critter reads it as its task spec.
Beyond the built-in create and review flows, you can define custom critter types in critters.config.yaml. Each type gets its own trigger label, phase pipeline, model, tools, and outcome statuses.
critterTypes:
code-audit:
trigger: { label: "Code Audit", status: "Todo", statusType: "unstarted" }
repo: { clone: true }
phases:
- name: audit
prompt: ~/.critters/prompts/code-audit.md
model: sonnet
maxTurns: 20
tools: [Read, Glob, Grep, "Bash(git:*)", "Bash(ls:*)"]
outcomes:
success: { status: "Done", comment: true }
failure: { status: "Critter Failed", comment: true }
concurrency: 3
timeoutMinutes: 10Custom types automatically:
- Prompt Claude to write a
.critter-report.mdfile - Upload the report as a
.mdattachment on the issue - Post the report as an inline comment
Prompt files support {{identifier}}, {{title}}, {{description}}, and other variables. See CLAUDE.md for the full reference.
Assignee filtering: Add trigger.assignee to only pick up issues assigned to a specific user. Use an email address (e.g., "[email protected]") or "me" for the authenticated user. Useful in shared projects where you don't want critters picking up every labeled issue.
Quiet mode: Add quietComments: true to suppress status/progress comments (e.g., "picking up task...", "phase completed in..."). Report content and error comments are still posted.
Claim status: Critter types with trigger.statusType: "unstarted" automatically transition issues to "In Progress" when picked up, preventing duplicate dispatch. Override with claimStatus: "Reviewing" (or any status), or set claimStatus: null to disable. Types without statusType: "unstarted" (e.g., review types) don't claim by default.
Model guidance: Use sonnet or opus for custom types. Haiku often ignores tool-use instructions and produces shallow output.
If critterTypes is omitted from config, the daemon synthesizes the default create and review types from the flat config fields — fully backward compatible.
Critters is a general-purpose agent orchestrator — the built-in create and review types are just the start. Any workflow matching "watch for trigger → run Claude with a prompt → produce an outcome" can be a critter type:
- Issue triage bot — automatically labels, prioritizes, and summarizes new issues
- Documentation writer — reads code and generates or updates markdown docs, opens a PR
- Security auditor — scans for OWASP top 10 vulnerabilities, dependency issues, and hardcoded secrets
- Test generator — reads implementation code and writes missing test cases, opens a PR
- Multi-step workflows — chain critter types using Linear/Jira blocking relationships (e.g., a "plan" critter spawns sub-tickets, then "implement" critters pick up each one)
See CLAUDE.md for complete config examples.
A single daemon can poll both Linear and Jira. Set the default provider at the top level, then use provider on each critter type to override:
provider: linear # default
jiraStatusMap:
"Todo": "To Do"
"In Progress": "In Progress"
"In Review": "In Review"
"Done": "Done"
"Critter Failed": "Failed"
critterTypes:
create:
provider: [linear, jira] # polls both trackers with the same config
trigger: { label: "Critter", status: "Todo", statusType: "unstarted" }
repo: { clone: true, branch: true, commitPlans: false }
phases:
- name: planning
prompt: builtin:planning
model: opus
maxTurns: 50
tools: readonly
- name: execution
prompt: builtin:execution
model: opus
maxTurns: 75
tools: default
outcomes:
success: { status: "In Review" }
failure: { status: "Critter Failed" }
concurrency: 2
timeoutMinutes: 30provider accepts a single value ("linear", "jira") or an array ([linear, jira]). When an array is used, the type is expanded internally so each provider is polled independently — no need to duplicate config.
Only the env vars for providers you actually use are required. A Linear-only config doesn't need JIRA_* vars.
See CLAUDE.md for full multi-provider docs, Jira differences, and more config examples.
The github provider polls GitHub issues in a configured list of repos. Issues are identified as owner/repo#42 everywhere (logs, branches, CLI).
provider: github
github:
repos: ["myorg/api", "andrew/critters"] # ticket-source repos (required)
statusField: "Status" # org issue field for statuses (default)
statusMap: # optional: critter status → GitHub option/label name
"In Progress": "In progress"
statusTypes: # optional: statusType buckets for triggers
unstarted: ["Todo", "Backlog"]
started: ["In progress"]
completed: ["Done"]Auth: GITHUB_TOKEN (repo read/write + issues read/write; org admin only needed for auto-creating status field options). Webhooks: set GITHUB_WEBHOOK_SECRET and point a repo/org webhook (events: issues) at /webhook/github.
Statuses are dual-mode, detected per repo at startup:
- Field mode — for repos in an organization, Critters uses the org's single-select issue field named by
statusField(defaultStatus). Create the field once as an org admin (Settings → Planning → Issue fields) with options like Todo / In Progress / In Review / Done; missing outcome options are auto-created when the token has org admin rights. - Label mode — personal repos (or orgs whose fields the token can't read) fall back to
status:<Name>labels (status:Todo,status:In Progress, …), created and swapped automatically.
Limitations: no attachment uploads (logs are posted as inline comment excerpts instead); status changes made by humans in field mode are discovered by polling, not webhooks (GitHub emits no event for field-value changes); api.github.com only (no Enterprise Server); trigger.statusType only works when statusTypes buckets are configured — otherwise triggers match the exact status name. If you use teamRepos to map issues to clone URLs, key it with the same casing as github.repos (lookup is case-sensitive).
gh version: use gh ≥ 2.96 (or at least newer than 2.45). Older releases — including the gh 2.45.0 shipped by Ubuntu 24.04 — fail gh pr edit with GraphQL: Projects (classic) is being deprecated… (repository.pullRequest.projectCards) on some repos, which silently disables Critters' plan/stats injection into PR bodies (logged as Failed to update PR body with plan (non-fatal)). Everything else still works.
- Self-Hosting Guide — deployment, systemd, remote access, monitoring
- Configuration Reference — all config fields, environment variables, critter types, hooks
See CLAUDE.md for detailed developer docs, architecture diagrams, and contributor conventions.