The agent loop
The loop: someone captures a bug → it's assigned to a bot → the bot's webhook fires → your CI runs an agent with the Peekr MCP → the agent looks at the screenshot, fixes the code, opens a PR, and resolves the capture with the link. Nobody relays anything. This page is the recipe, with GitHub Actions and Claude Code, in two flavours.
The Peekr guide is included in MCP. Have the agent read get_peekr_guide at the start of a run, choose the intended workspace, and preserve the claim token returned when it takes an issue. Bots keep their own scoped access; personal settings and invitations are not part of a bot's grant.
1. Create the bot
Settings → Bots → New bot. Name it (“Claude Code (CI)”), give it only its groups (a guest) so it sees the groups you share and captures assigned to it, and save the pk_bot_… token as a repository secret called PEEKR_BOT_TOKEN.
Then, on the groups it should work: Settings → Groups → add the bot as a member, and optionally set it as the group's default assignee so every new capture there is its job.
2a. Simplest: poll on a schedule (no infrastructure)
# .github/workflows/peekr-agent.yml
name: Peekr agent
on:
schedule: [{ cron: "*/15 * * * *" }]
workflow_dispatch:
concurrency:
group: peekr-agent
cancel-in-progress: false
jobs:
fix:
runs-on: ubuntu-latest
permissions: { contents: write, pull-requests: write }
steps:
- uses: actions/checkout@v4
- run: npm install -g @anthropic-ai/claude-code
- run: |
cat > peekr-mcp.json <<EOF
{ "mcpServers": { "peekr": { "type": "http", "url": "https://mcp.peekr.dev/mcp",
"headers": { "Authorization": "Bearer $PEEKR_BOT_TOKEN" } } } }
EOF
env: { PEEKR_BOT_TOKEN: ${{ secrets.PEEKR_BOT_TOKEN }} }
- run: |
claude -p "Read get_peekr_guide. Use this bot's workspace. List open captures assigned to you. For each: claim it, \
look at every screenshot, find the root cause in this repo, fix it on a branch named \
peekr/<short id>, run the tests, open a PR with 'gh pr create', comment your diagnosis \
on the capture, and resolve it with the PR URL and current claim_token. Respect human review. \
Skip work already claimed by another agent. If you can't fix one, comment why and leave it unresolved." \
--mcp-config peekr-mcp.json --allowedTools "mcp__peekr__*,Bash,Edit,Write,Read"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}This asks GitHub to start a run every fifteen minutes; scheduling can be delayed. The concurrency group prevents overlapping workflow runs. Verify one manual run before relying on the schedule.
2b. Instant: the bot's webhook triggers the workflow
GitHub can start a workflow from an HTTP call (repository_dispatch), but it needs a GitHub token in the request, and Peekr's webhooks carry only their own signature. So put a tiny relay in between — a Cloudflare Worker works well — that verifies Peekr's signature and calls GitHub:
// worker.js — verify Peekr's signature, then start the workflow
export default {
async fetch(req, env) {
if (req.method !== "POST") return new Response("method not allowed", { status: 405 });
const body = await req.text();
const ts = req.headers.get("X-Peekr-Timestamp");
const sig = req.headers.get("X-Peekr-Signature"); // "sha256=<hex>"
if (!ts || !/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return new Response("stale timestamp", { status: 401 });
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(env.PEEKR_WEBHOOK_SECRET),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${ts}.${body}`));
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
if (sig !== `sha256=${hex}`) return new Response("bad signature", { status: 401 });
const event = JSON.parse(body);
if (event.event !== "capture.assigned") return new Response("ignored");
const dispatched = await fetch(`https://api.github.com/repos/${env.GH_REPO}/dispatches`, {
method: "POST",
headers: { Authorization: `Bearer ${env.GH_TOKEN}`, Accept: "application/vnd.github+json", "User-Agent": "peekr-relay" },
body: JSON.stringify({ event_type: "peekr-assigned", client_payload: { capture: event.capture_id } }),
});
if (!dispatched.ok) return new Response("dispatch failed", { status: 502 });
return new Response("ok");
},
};Set the bot's webhook URL to the worker, put the signing secret Peekr showed you in PEEKR_WEBHOOK_SECRET, and change the workflow trigger to:
on:
repository_dispatch: { types: [peekr-assigned] }…and pass the capture ID to your agent through an environment variable, after validating its UUID format; do not interpolate request data directly into shell commands. Run a test assignment and verify your relay and workflow before relying on unattended delivery. This minimal relay demonstrates signature and dispatch handling. Production relays also need durable deduplication of event_id, request-size limits and monitoring. A failed GitHub dispatch returns an error so Peekr can retry.
3. Keep a person in the loop
- Set the workspace to propose (Settings → General) and the bot can't mark anything resolved on its own: it proposes, the author gets a notification, and one click confirms.
- Every action shows the bot's name on the board and in the activity history; nothing it does is silent.
- Rotate the bot's token from Settings → Bots if it ever leaks; the old token stops immediately.
Other agents
Nothing here is specific to Claude Code. Any agent that speaks MCP over HTTP with a bearer header works the same way; see Connect an agent for Cursor, VS Code and Windsurf, and the API if you'd rather script it without MCP.