Backed by Y Combinator

ClaudeCodexCursorCopilotClaude writes your code.Canary tests it.

Canary is the independent tester for code your agents write. It runs your app, tries to break every change, and reports what would have reached production.

Give this to your agent.
Claude Code— pocketbase
Database-aware health endpoint
Add a database-aware health endpoint: GET /api/health should ping the data and auxiliary databases and answer 503 when either cannot run SELECT 1.
apis/health.goUpdated
Done. GET /api/health runs SELECT 1 against the data and the auxiliary database before answering. If either probe fails, the endpoint logs the error and returns HTTP 503 with code 503 and message "API is unhealthy." Each probe is bounded by a 3 second timeout derived from the request context. The successful response, including the superuser-only fields, is unchanged.
∞ Agent Claude Code
@@ -1,9 +1,12 @@
package apis
import (
+ "context"
"net/http"
"slices"
+ "time"
+ "github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
@@ -14,7 +17,12 @@ func bindHealthApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
subGroup.GET("", healthCheck)
}
-// healthCheck returns a 200 OK response if the server is healthy.
+// healthCheckDBTimeout is the max time the health check waits for each database ping.
+const healthCheckDBTimeout = 3 * time.Second
+
+// healthCheck returns a 200 OK response if the server is healthy
+// (aka. the HTTP server is up and the data and aux databases answer a trivial query),
+// otherwise a 503 Service Unavailable response.
func healthCheck(e *core.RequestEvent) error {
resp := struct {
Message string `json:"message"`
@@ -25,6 +33,16 @@ func healthCheck(e *core.RequestEvent) error {
Message: "API is healthy.",
}
+ if err := pingHealthDBs(e); err != nil {
+ e.App.Logger().Error("Health check database ping failed", "error", err)
+
+ resp.Code = http.StatusServiceUnavailable
+ resp.Message = "API is unhealthy."
+ resp.Data = map[string]any{}
+
+ return e.JSON(http.StatusServiceUnavailable, resp)
+ }
+
// @todo evaluate whether it is worth removing the extra info from the health endpoint
if e.HasSuperuserAuth() {
resp.Data = make(map[string]any, 3)
@@ -52,3 +70,30 @@ func healthCheck(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, resp)
}
+
+// pingHealthDBs runs a trivial query against the data and aux databases
+// and returns the first error it encounters.
+//
+// Each database gets its own healthCheckDBTimeout budget (derived from
+// the request context so that a client cancellation still stops the probe).
+func pingHealthDBs(e *core.RequestEvent) error {
+ if err := pingHealthDB(e.Request.Context(), e.App.DB()); err != nil {
+ return err
+ }
+
+ if err := pingHealthDB(e.Request.Context(), e.App.AuxDB()); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// pingHealthDB runs SELECT 1 against the provided db within healthCheckDBTimeout.
+func pingHealthDB(parent context.Context, db dbx.Builder) error {
+ ctx, cancel := context.WithTimeout(parent, healthCheckDBTimeout)
+ defer cancel()
+
+ var result int
+
+ return db.NewQuery("SELECT 1").WithContext(ctx).Row(&result)
+}
0K+real bugs, not lint
bugs found before merge
0K+
engineer hours saved
0%+
fix rate
0%
rated P0 / P1
01Install

Get started in under 2 minutes.

Works with Claude Code, Cursor, Codex and any of your agents
01
Paste the prompt
02
Connect your stack
03
Catch failures
Paste this into your coding agent
Install the Canary CLI with `npm i -g @runcanary/cli`, then run `canary skills` and follow its instructions to onboard this repository.
02The problem

The blast radius is the unknown unknown.

  1. 01

    Your agents raise more PRs than your team can review. Review turned into a skim.

  2. 02

    The tests pass. The same agent wrote them, and nobody on your team reads them. Nor should they.

  3. 03

    The bug shows up as a Slack message from a customer, or a page that wakes your on-call at 2am.

  4. Teams on Canary have seen bug reports on new code go down, and on-call pages with them.
03How it works

Your code was read run.

One loop. From your coding agent, and on every pull request.

Reviewers read the diff. Canary runs the app, tries to break the diff, and reports the blind spots you missed.

01 / 04Read
01

Canary reads the diff and your codebase and maps every flow the change can reach.

▸ diff: apis/health.go · +46 −1
▸ reach: GET /api/health · backups page · superuser page · proxy header
▸ 5 tests queued
$
02

It boots your app in a clean sandbox and tries to break every flow it mapped, in real browsers and APIs.

▸ GET /api/health with the database down … running
▸ backups page while health returns 503 … running
▸ aux probe after the data probe times out … running
$
03

Every failure comes back reproduced: a replay and a one-line root cause.

✗ aux database inherits the data probe's spent timeout
✗ three dashboard pages go stale after one 503
root cause: one deadline shared by both probes
$
04

It re-tests the fix until it holds, then keeps the test as a regression on every pull request.

✓ fix holds · re-tested
✓ regression armed: health.probe-timeouts
ready to merge.
$
04Where it runs

Canary at every stepof your dev cycle.

Two triggers. One loop. Every run ends in a report.

01 / 04In your agent
01

Before your agent calls a task done. Canary snapshots the uncommitted tree, boots it in a clean sandbox and tests it. No commit, no PR.

$ canary verify
▸ snapshot: working tree · 4 files · nothing committed
▸ sandbox: clean boot · pocketbase
▸ 5 tests queued … running
$
02

Runs when the PR opens. One comment per failed check, with the replay and the root cause on the line that caused it. Rerun it from a comment.

github · pull request opened
▸ booting the branch in a clean sandbox
✗ 2 checks failed → 2 comments, on the line
▸ rerun requested from the PR · re-checking 2 … running
$
03

The agent reads every finding. It fixes what it can, disputes what it should with a reason, and re-checks until the change holds.

$ canary verify --failed f1e29de4
✗ aux database inherits the data probe's spent timeout · Medium
→ fixed: each probe gets its own 3s deadline
▸ disputed 3 · Low · pre-existing dashboard behaviour, reason attached
$
04

Every run ends in a report: fixed and re-verified, disputed and why, and a ready-or-not verdict. In your chat, in your repo, and on Canary with proof for every check.

report · master · run 119ace16
✓ verdict: ready · 2 iterations · 5 checks
▸ fixed 2 · disputed 3 · open 0
▸ .canary/reports/master/report.html
▸ runcanary.ai/runs/119ace16
$
05Integrations

Works where you work.

Canary reads the context you already have, and files every break back to your coding agent.

GitHub
Linear
Sentry
Datadog
Notion
Slack
CANARYtests
Claude Code
PR · CLI · MCP
06Examples

Every failure arrives withproof.

Four failures Canary caught on real customer pull requests.

01 / 04Wrong org's file
test replay · api/documentsREPLAY
test · api/documents
▸ auth as Org A · request a review image by storageId
▸ swap storageId → a document owned by Org B
✗ server resolves it · Org B's private file returned to Org Abroke here
· one customer can open another customer's file
$
Case file · api/documentsP0
staging · found autonomously in 6m 09s
▸
Test
swap storageId to another org's document
✗
Break
another org's private file opens for the caller
⌖
Root cause
image-URL resolver looks up by storageId with no org-ownership check
+
Suggested fix
− resolveImage(storageId)
+ resolveImage(storageId, { orgId: caller.orgId })
✓
Regression armed
cross-org.storageId reruns on every PR
replay · ui/formsREPLAY
Studio
new report · draft
Report name
Q3 rollout
chart.pnguploading…
Create reportdouble-tap · Δ80ms
✓ report created00:04
✓ report created (duplicate)00:05
▶ 00:04 / 00:06▮ break markers
Case file · ui/formsP1
staging · found autonomously in 3m 07s
▸
Test
double-tap Create before the first submit settles
✗
Break
the record is created twice
⌖
Root cause
submit stays enabled during an in-flight upload: one isUploading flag misses concurrent submits
+
Suggested fix
− disabled={isUploading}
+ disabled={pendingUploads.size > 0}
✓
Regression armed
forms.create.double-submit reruns on every PR
test replay · api/webhooksREPLAY
test · api/webhooks
▸ register a callback URL → http://169.254.169.254/…
▸ trigger the webhook · server fetches the target
✗ the server fetches it · cloud credentials come backbroke here
· sensitive record data travels in the payload
$
Case file · api/webhooksP0
staging · found autonomously in 8m 36s
▸
Test
point a callback URL at the cloud-metadata endpoint
✗
Break
the server fetches whatever URL it is handed, internal ones included
⌖
Root cause
callback validated by scheme only: no block on loopback / private / link-local IPs
+
Suggested fix
− fetch(callbackUrl)
+ assertPublicDestination(callbackUrl); fetch(callbackUrl)
✓
Regression armed
callback.ssrf reruns on every PR
test replay · api/jobsREPLAY
test · api/jobs
▸ DELETE /jobs/{id} with id = '.'
▸ id path-joined into the job-store directory
✗ '.' resolves to the store root · entire job store deletedbroke here
· one call wipes every job
$
Case file · api/jobsP0
staging · found autonomously in 7m 52s
▸
Test
DELETE a job with id = '.'
✗
Break
the whole job store is removed, not one job
⌖
Root cause
job id path-joined and deleted without normalization: '.' escapes to the store root
+
Suggested fix
− rm(join(store, id))
+ assertSafeId(id); removeByKey(id) // reject '.', '..', separators
✓
Regression armed
jobs.delete.pathsafe reruns on every PR
07Research

Field notes from the frontier.

We publish what we learn measuring AI against real, messy codebases.

Benchmark15 min read · March 2026

QA-Bench v0: measuring how AI models handle code verification

Given a real pull request on a production codebase, can a model find every affected user flow and catch what breaks? We put a purpose-built agent against the frontier models across 35 PRs on four production-scale repos.

Read the benchmark →
Overall accuracy · QA-Bench v0
Canary
83.1
GPT-5.4
80.2
Claude Code
78
Sonnet 4.6
73.2
08Your move

Find it before your users do.

Put the flock on your next pull request. It runs on every one after.

Give this to your agent.