drums
Talk to us
Documentation

Documentation

Drums is a self-improving product. It watches how people use your product live, finds what the code can address, makes a change with your approval, using the coding agent you already use, and measures whether it actually helped. The loop: observe, understand, hypothesis, change, verify, roll out, measure, learn. Where the observation is a failure, Drums makes it happen again before anything is written.

Status. Drums is in active development with design partners. The reproduce loop (detect, attribute, rebuild, replay) ships today. Repair and release are rolling out to design partners first.

Observations. The observation loop (Observation → Hypothesis → Change → Outcome) has its first stage live. After every deploy, drums watch reads its own record and writes rate-shift observations, "error events went from A per hour to B per hour after this deploy." Plain facts: no severity, no diagnosis. The local dashboard shows them on its Observations page.

Hypotheses. drums hypothesize turns an observation into a recorded hypothesis with its evaluation plan. The judgment stays yours; Drums records it as inferred. drums hypothesis accept/reject decides.

Measurement. drums change freezes the plan and the baseline. When the declared window fully closes, drums watch measures the outcome once and writes it beside the change, never re-cut afterward. error_event_rate comes from Drums’ own record with zero configuration. completion_rate and abandonment come through the PostHog you already run (posthog_host + posthog_project in config, the key in DRUMS_POSTHOG_API_KEY, never in a file). A due change whose source cannot be read is skipped and retried, never invented. A change that cannot be measured is "shipped, outcome unmeasured", said in as many words. A metric that moved is a measurement, not by itself proof the change caused it. Drums states what was measured and when, and never assumes cause.

Revisits. A verdict is what was measured then. At 7, 30, and 90 days after ship, Drums re-measures each change (the same metric, the same frozen yardstick, a later window) and appends what it finds beside the original outcome, never over it. A change that looked good when its window closed and quietly regressed by day 30 produces a revisit line and a Learning notification. Nothing is forgotten because the PR closed.

Product Bets. drums bet create pre-registers a product decision (belief, evidence, audience, expectation) before the outcome exists. drums bet confirm puts it on record under a human's hand. When the chain's outcome lands, the watch tick evaluates it: supported, not supported, or inconclusive. Each verdict says how sure it can be about cause, and a full rollout can never claim more than low. drums bet learn closes it with what the team took from it.

Drafts and questions. drums draft lets Drums draft the bet itself, on the coding agent you already use, validated like a human's, proposed, never confirmed. With proactive_draft = true and an agent on the machine, a new observation triggers a draft on its own, consent-gated, because it spends your agent's tokens. drums ask answers questions over the record through a rebuildable index in .drums/ that never becomes a second source of truth.

Notifications. With a Slack webhook configured, the daemon sends four kinds of message and no other vocabulary: FYI (investigated, nothing needs you), Learning (a prior bet matured), Working (found something worth investigating), Decision (work complete; production approval required). One attempt each, a courtesy copy of the record, never the record itself.

The dashboard renders the loop on its Mirror, Bets, and Memory pages. Nothing on this page is a claim of general availability.

Get started

Install

One binary. macOS today.

terminal
$ curl -fsSL getdrums.app/install | sh
installed drums → /usr/local/bin/drums

What it assumes

  • A git repository. Drums reads it to work out what your project is and how it ships.
  • git on your PATH. Drums itself is a single binary, it does not need Node, Python, or a package manager.
  • Whatever your project already uses to run tests and to deploy. Drums drives those; it does not replace them.

macOS only for now, on Apple Silicon and Intel. Linux and Windows are not published yet, curl -fsSL getdrums.app/install | sh refuses them by name rather than installing something that will not run.

Installing changes nothing. Drums starts in observe-only and says so, see the quickstart below.

drums watch requires an account: run drums login first, which is one browser approval. drums init needs none, it runs the loop once locally and is there so you can watch Drums work before signing up for anything. DRUMS_NO_ACCOUNT=1 runs watch without an account for CI and air-gapped machines.

Signing in is checked once at startup against the console. A rejected credential refuses; an unreachable console does not, the local loop does not depend on our uptime, which is the whole argument for running it on your own machine.

The first drums watch prints a one-time notice about the anonymous install heartbeat before it sends anything; DRUMS_TELEMETRY=off sends nothing at all.

Get started

Quickstart

Two commands. The second one ends by proving a real event reached the record.

1

Install

terminal
$ curl -fsSL getdrums.app/install | sh
2

Set up

drums init detects the framework, writes the config, installs the reporting snippet, and then proves the path works by sending a real event and reading it back out of the record. It prints the plan and waits before writing anything.

The last thing it prints is the two things it cannot do for you, because an install that looks finished but reports nothing is worse than one that says what is missing.

~/your-repo
$ drums init

drums 0.1.0 · ~/checkout-api

  git repository
  express, package.json declares `express` as a dependency [observed]
  entrypoint server.js
  claude, the agent that will write repairs

This will:
  + .drums/config.toml           what `drums daemon start` reads
  + drums-report.js              the reporting middleware
  ~ server.js                    2 lines appended, mounts the reporter

Proceed? [Y/n]

Verifying the path from your app to the record…
  a test event travelled http://127.0.0.1:63921/v1/events → the
  record at .drums/record.jsonl [verified]

Set up. Start the loop:

  drums init
  drums login
  drums watch
3

Watch

Drums detects failures, attributes them to a deploy, reproduces them, and repairs them. It proposes; nothing ships without drums ship.

On a real terminal this is a live view, one card per failure, updated in place, with a chip on every claim saying how it is known. Piped, scripted, run under CI, or with --plain / DRUMS_PLAIN=1, it is the plain line-by-line narration.

~/your-repo
$ drums watch --plain
watching ~/checkout-api · ingest :7787 · repairs propose only — nothing ships without `drums ship`.
Get started

BoundariesIn design

The line between safe and risky is the most important setting in the product. It is also the one nobody will fill in a form for, so Drums proposes it and takes corrections in plain language.

Not shipped yet: there is no drums boundaries command today, and the session below is the intended shape rather than a transcript. What DOES exist is the autonomy ladder, authority earned per failure class, granted by a human with drums authority promote, and taken away automatically on one rollback.

planned interface · `drums boundaries` is not in the CLI today
$ drums boundaries

Based on your reverts, code owners, and migration
history, here is what I would treat as risky:

  always ask a person
    billing/**            owned by @rishi, 2 reverts
    lib/auth/**           touches sessions
    prisma/migrations/**  not reversible
    infra/**              no test coverage

  safe to repair alone
    app/**  components/**  lib/format/**
    reversible, covered by tests, no prior incidents

Sound right?

$ drums boundaries "also ask before touching anything under app/checkout"
✓ checkout moved to always ask.

Always ask, by default

Four classes require a named human no matter what the track record says, and Drums applies them before it has read anything:

Customer dataBillingPermissionsInfrastructure

Everything else starts in observe-only and moves only as far as you allow. The structured version of your boundaries exists underneath and is readable with drums boundaries --show (in design (not in the CLI yet)) it is never the thing you have to write.

How it works

The loop

One observation, eight stages: observe, understand, hypothesis, change, verify, roll out, measure, learn. The model is that safe, reversible classes eventually complete without waiting for an engineer; today approval is the default at every rung and nothing ships itself that has not earned it.

StageWhat happens
observeSomething people did, or the product did, is recorded from your telemetry as a fact, with the evidence that says so.
understandThe observation is connected to product state and code, for a failure, the deploy that shipped before it and the files it changed.
hypothesisWhat to change, and the outcome the change is expected to move. For a failure, the expected outcome is that the failure stops.
changeYour own coding agent writes it, given the evidence and the acceptance criteria.
verifyIndependent proof before anything ships. Where the observation is a failure, a container is built at that exact revision and the captured request replayed, if it does not fail, Drums says so and stops.
roll outThe change goes out through your existing deployment platform (to a small share of traffic first where the platform supports it) with approval as the default.
measureThe outcome is compared against the frozen baseline over the declared window. The record says what was measured, a moved metric is not, by itself, proof the change caused it. Rolling back stays one human-run command (drums revert); automatic rollback on regression is not shipped.
learnThe record keeps the whole chain (observation, hypothesis, change, outcome) and authority for the next change rests on that track record.

How Drums says what it knows

Every claim Drums makes carries how it is known. These five states appear in the CLI, in notifications, in the record, and in the audit export.

verifiedDrums ran something and watched it pass.
observedTelemetry says so.
inferredA model concluded it.
approvedAn identified human signed off.
unresolvedDrums does not know, and says so.
A repair ships on its own only when every claim supporting it is verified or observed. Anything resting on an inferred claim goes to a person. Attribution is inferred, which is why reproduction exists.

Autonomy is earned narrowly, per class

Drums observes broadly and earns authority narrowly. Each class of failure, identified by its service and error name, climbs the ladder independently, on its own track record. Drums proposes the promotion and shows the evidence; you decide. Nothing promotes itself.

observeDrums watches and says what it would have done. Changes nothing.
shadowRepairs are generated and verified in isolation, never shipped. You compare them against what your team actually did.
proposeA verified repair waits for a person. The default, and a success state.
act aloneDrums ships, verifies, and reports. You read about it afterwards.

--repair auto is consent, not a grant: it raises the ceiling to act-alone, and the class still has to have earned it. Five consecutive repairs that shipped and stayed shipped earns a promotion proposal; drums authority promote <class> applies it. That command refuses a class that hasn't earned it, there is deliberately no override, because a flag that granted autonomy would make the ladder decorative.

Act-alone is the paid capability

Everything below act-alone is free, forever, on your own machine, watching, understanding, changing, verifying, proposing. Everything up to and including a verified repair waiting for you at drums ship needs no key and no account. The one thing that is paid is the last rung, letting Drums ship a failure class without waiting for you, and drums authority promote is the only command that asks for a license.

It asks in that order on purpose. A class that hasn't earned act-alone is refused on the evidence, and money is never mentioned; the license question only comes up once your own production record shows five consecutive clean ships. Nothing else in the product reads the license, so an expired or absent key never stops a repair being produced, verified, or proposed, it only withholds the grant.

Verification is offline. A key is an Ed25519 signature checked against a public key compiled into the binary: no license server, no activation call, no heartbeat. A product that stops working when our servers do is not one you should deploy in front of production, and a binary that phones home with a customer identifier would contradict everything on this page about the boundary. The trade-off is stated plainly: an offline key cannot be revoked mid-term, which is why keys carry an expiry. drums activate <key> stores one; a key that is malformed or expired grants nothing and says exactly why.

Demotion is automatic, immediate, and loud. One rollback or failed ship drops the class straight back to propose the moment it is recorded, and the terminal says so. Earning authority takes a human and a streak; losing it takes one mistake and no meeting.

The whole ladder is folded from the append-only record, so a restart rebuilds it and there is no separate state file that could disagree with your audit trail.

When the ladder says ask

A repair that needs a human does not sit in a terminal waiting to be noticed. The job is created and held (nothing is dispatched into your CI and nothing is pushed) and a link is opened that can go into a Linear comment, a Slack digest, or an email. Opening it shows the repository, the failing signature, the commit, and why you are being asked. Two buttons: approve, or reject.

The link is an address, not a credential. Holding it proves nothing: answering requires being signed in to app.getdrums.app with at least the approver role on the account that owns the repair. This is not ceremony, links travel. A Linear comment is readable by a whole workspace and Slack forwards, so a link that authorized on its own would be an approval anybody who scrolled up could replay.

Because the link is only an address, you do not need it in hand to answer. The Approvals page in the console lists everything waiting, filtered by pending, approved or rejected, and offers the same two buttons on each. Both routes run the same check against the same row: the role that decides is the one you hold in the account that owns the repair, and whichever of you answers first is the one recorded. What the console still cannot do is show you the link, it stores only a hash of it, so a console that could re-print one would be a console that could leak one.

Every claim carries how Drums knows it. Approve and Reject sit at the bottom of the expanded row.

The decision is also never a GET. Slack, Linear and most mail clients fetch a URL the moment it is posted to render a preview card; if following the link approved anything, posting it would approve the repair before a person ever read it. Rendering is a page, deciding is a form.

One approval per repair, answered once, recorded with the address of the person who answered and the time they did it. A rejection cancels the repair outright. Approvals expire after seven days, so a link found in a channel a month later does nothing.

How it works

RoutinesIn design

Scheduled maintenance (a dependency upgrade, a flaky test hunted down) described the way you would describe it to a colleague. There is no drums routine command today and nothing runs on a schedule.

It is described here rather than built because a routine cannot carry the property the rest of Drums is organised around. Upgrading a dependency and running the suite has no failure to reproduce and nothing to prove, so the strongest claim such a run could make is that the suite is no worse than before: non-regression, never verified. That is a real bar, and it is the one --repair-reported already applies to issues a person filed, which have nothing to replay either. It is simply not the bar that makes this different from a scheduler.

The machinery exists and the shape is settled. It will be built when somebody who is paying names it as the reason, and until then this section says so rather than showing an interface nobody can type.

How it works

Stop and undo

One control, reachable from everywhere, that a frightened engineer can find in three seconds.

There is no single drums stop or drums undo command yet In design. What stops Drums today: ctrl-c or SIGTERM on drums watch tears the watch down immediately, its worktrees are cleaned up and any in-flight agent process groups are killed.
~/your-repo
$ drums watch
watching 3 environments · ctrl-c to detach

^C
stopping · cleaning up worktrees · killing in-flight agent process groups
stopped.

Reversing a shipped repair with one command (and a bulk drums undo --since) is In design. Repair and release are themselves Rolling out to design partners; see the status note in the introduction.

How it works

Inspect

Git is the record. Changes land as real commits; the evidence rides the pull request as a claims table (what was seen, what was changed, how it was verified, who approved) and drums record replays the whole history. A git-notes channel that travels with the repository is in design.

~/your-repo
$ git log --oneline -1
a91f22d repair: guard optional promo field

$ drums record --limit 1
  failure      POST /api/checkout · TypeError · verified
  attributed   8f32a1 "add promo code field" · verified
  reproduced   container 8f32a1 · exit 1 · signature match · verified
  repair       a91f22d · replay passed · verified
  proposed     waiting at drums ship · approved

drums why

Blame, except it knows about production. Mined straight from the record, per-file failure history, newest first, every occurrence carrying its own how-it-is-known chip. A line or column is accepted and noted, but matching stays file-level for now: history isn't split out per line yet.

~/your-repo
$ drums why lib/cart/total.ts:41
  matching is per-file, line 41 is not tracked separately.

  2025-07-20 08:32 UTC · TypeError in shop (POST /api/checkout) [observed]
  2024-01-01 00:00 UTC · TypeError in shop (POST /api/checkout) [observed]

  this file has failed in production twice in 570 days

drums ps In design

A daemon doing invisible work is why teams refuse production access. drums ps will show what is running right now and the authority each task is operating under.

~/your-repo
$ drums ps
  TASK          STAGE         AUTHORITY      ELAPSED
  export-pdf    reproducing   propose-only   1m 12s
  dep-upgrade   verifying     act-alone      6m 40s
  2 running · 0 waiting on approval · 0 in canary
Ecosystem

Integrations

Drums is not a coding agent, not a monitoring product, and not a deployment platform. It drives the ones you already run.

Coding agents

Claude Code, Codex, Gemini CLI, Cursor, Amp and OpenCode write the change, each driven in its own documented headless mode. Detection is first-on-PATH, and DRUMS_AGENT_CMD overrides the lot. Drums decides what to change, whether it actually helped, and whether it ships; being agent-neutral is worth more than owning that step. An MCP server, so an agent can call Drums mid-session: reproduce this, verify this patch, tell me what broke last time someone touched this file, is in design.

Claude CodeCodexGemini CLICursorAmpOpenCodeMCP server in design

Telemetry in

Today observations come from Drums' own capture, the reporting snippet in your application and the deploy webhook, plus issues people report through Linear or Agentation. Drums reads the PostHog you already run to measure whether a shipped change helped; readers that produce observations from analytics are still in development. Either way they are inputs Drums consumes, never products it replaces. Drums has no graphs, alert rules, or on-call schedule of its own.

OpenTelemetryPostHogSentry

Deploys out

Canary and promotion happen through your existing deployment platform. The moment Drums requires its own deploy target, the install stops being five minutes. The traffic also runs the other way: Railway and Vercel can be connected as read-only sources of what shipped, which is what attribution needs.

Slack and Linear

Where Drums says what it found and asks what it may do. Both are optional, the record is in the console and in git log either way, and neither can act on your behalf. Connecting Slack does not give anyone in Slack the power to approve anything.

SlackLinear

Connect them in the console under Integrations → Slack and Linear. Press Connect, approve the screen on Slack's or Linear's side, and you land back on the same page with the workspace named. Approver or owner, the same bar as answering an approval, deciding where a question is delivered is part of answering it. Disconnect sits in the same row, and it tells Slack to revoke the bot token as well as forgetting it here.

Drums asks Slack for five scopes and nothing else. chat:write to post. im:write to open a direct message, so an approval can be addressed to a person rather than dropped into a channel. channels:read so the console can show you #alerts instead of C0123456789. app_mentions:read to receive @drums …. Narrow on purpose, because it delivers only messages that name the app and never the channel's conversation; the obvious alternative, channels:history with a message subscription, would send every message in every connected channel to our endpoint in order to find the few addressed to us. And commands, which grants the right to have /drums exist at all and reads nothing.

It does not ask to read your conversations or your member directory. users:read.email is deliberately absent, which is why Drums cannot address an approval to a Slack account by email address: reading a workspace's directory to send one message is a bigger ask than the feature is worth.

A scope granted after a token was issued does not reach that token, so a workspace connected before a scope was added has to be reconnected from the console before the feature that needs it works. That is visible and recoverable; asking for a permission up front with no feature behind it is not.

On Linear, Drums writes as the app rather than as whoever clicked Connect, so machine work never lands under a person's name.

What gets announced where

EventSlackLinear
failure_reproducedThe channelComments on the issue that reported it, or opens one, if you named a team
approval_neededA direct message, carrying the approval linkComments on the known issue
repair_shippedThe channelComments on the known issue
nothing_establishedNothingNothing
digestThe channelNothing

An approval that cannot be addressed to a person is dropped, not posted to a channel. That is deliberate. An approval is a question for someone: put it in a channel and it becomes everybody's problem, which is the reliable way to make it nobody's, the repair sits there while four people each assume one of the others is looking at it. So when Drums cannot work out who to write to, it records that and stops. A fallback to the channel would silently undo the one decision this table exists to make, on the event where being wrong costs the most.

An approval arrives as a plain link, never a button, and following it approves nothing. Mail and chat clients fetch every URL in a message to build a preview card, so anything a GET request can do is something a preview card can do by accident. The link is an address, not a credential: the page it opens renders nothing at all until the reader is signed in and holds the approver role on that account, and the decision is a separate press from that page. It is also why there is no Slack button and no Slack interactivity endpoint, a Slack user id is not authority, and Drums has no way to turn one into a Drums identity that could stand behind a decision.

Nothing established is routed nowhere on purpose. It is a real outcome and it belongs in the record, but a tool that pings on every non-result is muted within a week, and a muted tool cannot tell you the one thing that mattered. The count still surfaces in the digest, where a number is the right shape for it.

Choosing which Slack channel is not in the console yet In design. Until it is, a connected workspace receives the direct messages and nothing else, the channel column above is routing that already exists in code, not a picker you can fill in today. The console says so on the row rather than reporting a workspace as connected and leaving you to work out why nothing arrives.

Asking Drums from Slack

A connected workspace can also be asked things. /drums status answers with the connected repositories, how many approvals are waiting, and the last repair; /drums failures lists the five most recent; /drums why <failure id> prints what that repair had to make true and what the run claimed; /drums help lists the rest. Anything unrecognised, including nothing at all, is answered with help rather than an error.

@drums is the same commands asked out loud, and it is deliberately not a second implementation. The mention is stripped and the remainder goes through the same parser, the same identity mapping and the same authority gate as the slash command. Two parsers drift, and the one that drifts is the one nobody tested. Read verbs answer anywhere Drums was invited: the answer carries no credential, and somebody had to invite Drums to that channel before it could be mentioned there at all.

A Slack user id is not an identity. It is genuinely Slack's, the request signature means nobody outside the workspace can forge one, and it still says nothing about who that person is on a Drums account. So the mapping comes only from rows a signed-in person wrote in the console: a Slack member id recorded on the approval rota, or the account that connected the workspace. Where there is no such row, Drums says it does not know who this Slack account is and stops. Nothing typed into the command is ever consulted for identity.

A mention will not settle an approval. approve and reject are refused outside a direct message with Drums, and the reason is what a mention does to the link rather than what it does to the decision: a mention is public, so an approval link pasted into a channel is an address everybody in that channel now holds, and everybody who joins later, and search, and the workspace export. The gate would hold either way. An owner entitled to answer would answer, the approval would be settled correctly, and the address would be public afterwards anyway, so refusing once that has happened is a note about it rather than a control. The refusal therefore comes before the decision runs, it names the two ways to answer that stay private, and it never repeats the link back into the channel. A slash command is private (its text goes to Slack and to us and is never posted) which is why the same words are allowed there.

Private is judged from the conversation id: one beginning with D is a direct message, and C, G and anything unrecognised are treated as a room. Default-deny on purpose: if Slack ever changes its id scheme, the failure is authority refused in a direct message rather than authority granted in a channel.

Deciding from Slack at all is owner-only and off by default, per workspace. An owner turns it on in the console; the grant is scoped to that one workspace link rather than to the account, and reconnecting Slack starts it off again. That is a higher bar than the console itself, which accepts an approver, because a Slack session is not a Drums session: anybody who takes over that session, or any workspace admin who can impersonate it, would inherit the authority. Three things have to hold at once: the workspace opted in, the mapped member holds owner, and the person is holding the approval link that was delivered to them. Drums keeps only a hash of that link, so it cannot be looked up from Slack; holding it is part of what makes the answer yours.

Platforms you connect in the console

Four more connections live under Integrations → Platforms, each one OAuth grant held per account, reconnecting replaces the live grant rather than adding a second. What they are for differs, and so does who may make the decision: the bar rises with what the grant can reach. Disconnecting stays at approver throughout, because nobody has ever needed protecting from revoking too much of their own access.

RailwayVercelSupabasePostHog

Railway

Connecting Railway is how Drums learns what shipped. Both resource scopes are viewer (workspace:viewer and project:viewer), so the grant can read deployments and their logs and cannot redeploy, promote or revert anything, which are actions the autonomy ladder puts behind an approval. Connecting is an approver's call for that reason; replacing an existing connection is an owner's, because swapping the credential silently repoints every future answer to "which deploy caused this" at a different production.

Deploy events are the point of it. Attribution correlates a failure to the deploy that preceded it by time and by changed files, so what Drums keeps from each deploy is small and deliberate: the deployment, project, environment and service ids, the status, the commit sha, and when it happened. The commit message is not stored, the sha is a key into a repository Drums can already read, and the rest is customer data with no use. A failure seen before any deploy is not attributed to the nearest one: it settles as unresolved and says so.

Connecting Railway is the whole setup. Drums reads your deploys through the grant itself, every five minutes, so the OAuth round trip is sufficient on its own.

This used to be untrue, and the change is worth stating rather than quietly correcting. The grant proved which workspace was yours and did not make deploys arrive; somebody had to add a webhook by hand in Railway's own project settings. That step was the one most likely to be skipped, and skipping it does not fail loudly, repairs keep working and simply stop knowing what shipped, so the attributed to a deploy claim quietly goes missing on a system that otherwise looks healthy.

The webhook still works and is still the faster of the two: it arrives when something ships rather than at the next read. It is now optional. Both routes write the same row keyed on Railway's own deployment id, so a deploy learned twice is stored once, and the console counts rows rather than naming a route, a count that said "on the webhook" would tell somebody their webhook was fine on a day the poller was carrying them.

Polling costs 12 requests an hour per connected account, whatever the number of projects, because it is one nested query rather than one per project. Railway rate-limits per token, and a per-project sweep on a free workspace would spend the entire hourly budget watching for deploys instead of leaving room for the logs a repair actually reads.

Vercel

Vercel has two OAuth products and this is the Integrations one, because the other identifies a person and cannot read a deployment. The permission set is read on three things and nothing else: deployment, project and team. Not environment variables, which would hand Drums every production secret in the team in readable form for no gain, the loop needs to know that a deployment failed, never what a value is. Not log drains, which are a write dressed as a read because they configure where somebody's logs are sent. Vercel takes the permission set from the integration's own configuration rather than from the request, so Drums records the expected set on the installation row: a later widening then shows up in data rather than only in a dashboard nobody diffs.

A build that fails prints its reason into the build events; a deployment that ships and then throws prints it into the runtime logs, and a runtime log line carries requestMethod, requestPath and responseStatusCode beside the message. That is what makes it evidence about a request rather than a line of text.

Connecting Vercel requires an owner, not an approver. Everything Linear's connect decides is where Drums writes notes about work it has already been permitted to do; this one grants standing read access to a team's deployments and runtime logs, which is production traffic, paths, status codes, and whatever the application chose to print. Letting an outside system read production is a data-access decision, and those belong with whoever answers for the account rather than with whoever happens to be on the approval rota that week.

An install begun on Vercel's side is refused. Vercel's marketplace has its own Add Integration button, and somebody who presses it arrives at our callback with a perfectly valid code and no state cookie. That cookie is the only thing binding an installation to the person who asked for it and to the account it should land on, so accepting the flow would be account linking performed by whoever started it. The console is the only supported entry point, and an install started from the marketplace is told to start again from there.

Supabase

OAuth2 with PKCE, and the challenge method is S256, plain is in the specification and is worth nothing here, since a challenge equal to the verifier is a verifier published in the authorize URL. The application is registered with three permissions: projects read, auth read, and database write. There is no secrets scope, no billing scope and no organization write.

Database write is the one grant on this page that is not read-only, and it is worth saying plainly why it exists: the point of connecting a database is that a repair can propose a change to it. Proposing is all it does. The statement is a diff an approver reads and approves like any other, and only text a person approved ever runs against your data. An agent's output is never applied directly, that would make the autonomy ladder decorative, in the one place where the mistake is not undone by reverting a commit.

Connecting requires an owner, one step further along than Vercel's reason: this is not a routing decision or even a data-access one, it is the customer deciding who may change their database.

PostHog

PostHog implements CIMD (a client id metadata document) and it changes the shape of the connection. There is no application to register and no client secret anywhere: Drums serves a JSON document at /oauth-client, and that URL is the client id. PostHog fetches it during the flow to learn the application's name and, the part that matters, the redirect URIs it is allowed to send a code to. With no secret, PKCE is not a second lock beside a first one, it is the only thing standing between an intercepted authorization code and a usable token, so it is mandatory, and S256.

Two scopes, both read: error_tracking:read for the exceptions themselves, and session_recording:read for what the person was doing when it threw. No person scope, no query scope, and no write of any kind, so nothing here can resolve an issue, change a dashboard or delete a recording. Connecting requires an owner: this is a customer's product analytics, which is data about their users rather than about their infrastructure. The reader feeds change measurement today, completion and abandonment baselines and outcomes over frozen windows; readers that turn analytics into observations are still in development, so detection still comes from the reporting snippet.

The ceiling, stated plainly: a PostHog $exception carries a stack trace but not the failing HTTP request (no method, no path, no headers, no body) so a PostHog-sourced failure is not replayable, and a repair whose only evidence is a $exception can never reach verified on its own. Drums cannot rebuild the request that caused it, cannot re-run it against a candidate fix, and therefore cannot show that the fix changed the outcome. What arrives this way points Drums at a failure rather than proving one. Reaching verified still needs a reproducer from a source that carries the request: the reporting snippet in your own app, a failing test, or a CI job. The data is rich about the symptom and silent about the cause, and nobody should conclude otherwise from how much of it there is.

What every connection here has in common

The OAuth state is bound to the person who started the flow and to the account they started it from, and it is checked by recomputing it from the session rather than by reading it back out of the state itself. A state that only proves the browser finishing the flow is the browser that started it proves nothing about whose account the result lands on: an attacker can begin a genuine connect in their own browser and get somebody else to load the finishing URL, and their workspace is then bound into the victim's account. An account pointed at somebody else's production attributes its failures to deploys it did not make.

Disconnecting revokes upstream wherever the provider allows it, and says so where it does not. Slack offers auth.revoke, so disconnecting Slack retires the bot token in your workspace there and then. Linear, Railway, Vercel, Supabase and PostHog expose no call an application may make to retire its own grant (an authorization is removed by a person in their own dashboard) so for those five, destroying our copy is the whole of what we can do. That does stop Drums using the credential, which is the part we can guarantee; it does not retire the grant, and that gap costs most on Supabase, where what is left behind is not a stale read of somebody's deploys. The asymmetry is written down rather than smoothed over, because a disconnect button that implied more than it did is the wrong thing to be vague about.

Agentation, reports from your users

Nothing in the paragraph above applies to this one. Agentation is a browser annotation widget: somebody using your software clicks the thing that is wrong and types what is wrong with it, and the widget POSTs that note to whatever string was passed as its webhookUrl prop. There is no OAuth, no provider holding a grant on your behalf, and therefore nothing upstream to revoke, the credential is one Drums issues to you.

Agentation

The URL is the credential. Agentation sends no signature header and offers no shared secret, so unlike GitHub, Slack and Railway there is no HMAC over the body to authenticate a delivery with, the address in the webhookUrl prop is the whole of it. Everything about how Drums handles it follows from assuming it will leak, because URLs do: they land in proxy access logs, in Referer, in browser history, in a screenshot of a settings screen, and in whatever your own error tracker records about a failed fetch.

So the address is bounded rather than protected. It is write-only: it can cause a report to appear on exactly one account, and it can do nothing else, it reads nothing, lists nothing, opens no session, mints no second key and cannot revoke the one it is. Deletions are the case worth naming: a delivery that says an annotation was removed is acknowledged and dropped, because a URL that could destroy rows is a URL that erases somebody else's reports the day it leaks. Drums stores only a SHA-256 of it, so the console shows it once at creation and then genuinely cannot show it again, not to you, and not to support. What is left afterwards is a short non-secret prefix, enough to name a key in an audit line without the audit line holding anything that works.

The one moment the address exists outside your hands. After it is dismissed the console can only show the prefix, because a hash is all it kept.

Rotating is honest about what it breaks. Creating and rotating are the same action, and it revokes before it issues so two addresses are never live at once. That means the moment you rotate, the URL sitting in your deployed site stops working, and it stays broken until somebody edits that site and ships. Reports filed in that window do not arrive later, Agentation does not retry a delivery it was refused. The console says all of this before the press rather than after it. Rotate because an address may have leaked, not to tidy up. Creating, rotating and removing are approver-or-owner, the same bar as every other integration.

The ceiling, stated plainly: a reported bug is not replayable. A report is one person's account of what they saw. Drums executes nothing from it and has no request to replay, the only intake that carries a replayable request is the reporting snippet in your own app. So nothing is reproduced, and a repair whose only evidence is a report cannot show that it changed the outcome: its verify claim stays unresolved unless a test covers the change. drums watch --repair-reported will attempt one, and it is propose-only for exactly this reason.

What a report is genuinely good at is locating. The note arrives with the page it was made on, Agentation's own description of the element that was clicked, that element's DOM path, the React component path it sits inside, and a short sample of the text around it , which is usually enough to put an agent in the right file on the first try. Locating is strong here; verifying is not, and the two are not the same claim. A report that leads to a fix a test then covers reaches verified on the strength of the test, never on the strength of the report.

The surrounding text is capped at 500 characters, and that number is a privacy bound rather than a storage one. It is unreviewed text taken from the screen of whoever was reporting, so on a billing or a profile page it can pick up an address or an order total by accident, and every member of your account can read what is kept. Its job is only to say which element was meant, which a sentence or two covers. It is not raised because values are being cut; that is the reason it exists.

What is kept is what that needs and no more: the event name, Agentation's annotation id, the note, the page, the element and its path, the component path and the surrounding text described above, the time the widget says it was made, and the time we received it. The query string and fragment are removed from the page address before it is written, the path names the screen, which is the part a repair needs, and the query string is where session tokens, signed links and api keys ride. Nothing is synthesised: an unparseable timestamp is stored as nothing rather than as now(), because a missing time is a gap and an invented one is a false statement no later reader can detect. There is no column for a request, a stack or a status code, and filling one from this payload would be recording a stranger's guess in a shape the rest of Drums reads as evidence.

Where it shows up

Today: the CLI, the daemon's local dashboard, the console at app.getdrums.app, and approvals in Slack, with git log as the audit trail. In design: a git remote (git push drums verifies the change and forwards to origin if it holds, the smallest install for a team that will not grant production access on day one), a tool inside your agent, and editor-gutter annotations on lines with failure history.

Ecosystem

Deployment modes

One product, three postures. The engine is identical; only where the work runs changes.

ModeWhere work runsWhat leaves
localYour own machineNothing about your code. The anonymous install heartbeat (eight fields, none of them yours) unless you turn it off
teamDrums cloud, with reproductions in your CIThe record and its metadata. A CI runner has no coding-agent CLI logged in, so the repair step reads a credential from your repository secrets, a claude setup-token against the subscription you already pay for, or an API key. Drums cannot read it: the GitHub App requests no secrets permission.
privateYour VPC or on-premisesNothing, Drums ships the control plane as an image. Same heartbeat, same opt-out

Reproduction is designed to run inside the customer boundary: the container, the replayed request, and the failing document stay put. Drums moves the record, not the material. Enterprise accounts can point Drums at their own Anthropic, OpenAI, or Bedrock agreement so no new AI subprocessor is introduced.

What moves in team mode, and what does not

drums watch --dispatch-repairs is the seam between local and team mode, and it moves exactly one stage: the repair. Everything above it stays on the machine you started it on.

StageWhere it runs with --dispatch-repairs
detectLocally. Your app posts to the watcher on 127.0.0.1 exactly as before.
attributeLocally. It reads your deploy history, which the runner does not have.
reproduceLocally, and first. The failing request is replayed against the rebuilt revision here, and the result decides whether anything is dispatched at all.
repair · verifyYour CI, against your code, with your agent credential from your repository secrets.

The runner is one workflow file you commit yourself, once, and that is deliberate: the Drums App does not ask for Workflows: write, so the answer to "can this vendor change our CI" is no and stays no. Fetch it, read it, commit it on your default branch:

terminal
$ curl -fsSL getdrums.app/drums-repair.yml -o .github/workflows/drums-repair.yml

A failure that did not reproduce is never dispatched. That is not a safety margin, it is the product: a repair attempted against a failure Drums could not make happen again is a patch for a program nobody watched break, and every claim downstream of it would describe something that was never observed. Reproduction is also the one stage that cannot move to the runner without losing the point, if the runner decided whether the failure was real, nothing local would be left to disbelieve it.

What travels is a job reference plus the material a runner cannot reproduce without: the failure signature, the attribution, and the captured request. That is the single exception to "Drums moves the record, not the material", it is stored only until the job finishes, and it is fetched by the workflow over GitHub's own OIDC rather than passed as a workflow input, inputs are visible in the Actions UI. No source code and no model key ever reach Drums.

By default every dispatch asks a person before anything runs: the console opens an approval, the terminal prints the link, and the job sits queued until somebody answers. It stops asking only when the failure class has earned act-alone and you passed --repair auto to say you consent to it, the ceiling and the earned rung both have to agree, which is the same rule local mode applies to shipping. If the console is unreachable or refuses, the line says so and the watch carries on: the local loop's value does not depend on the hosted half being up.

What local mode costs

Nothing, on as many repositories as you like, with no time limit and no account. Local mode runs the whole loop (detect, attribute, reproduce, repair, verify, propose) on your machine, through your own coding-agent CLI and your own model key. We never resell models and never ask for your API key.

The paid boundary is act-alone authority and the hosted half: letting Drums ship a failure class without waiting for you, with the record and the approvals shared by your team. The ladder has to have earned it first, which means you can evaluate Drums against your own incidents indefinitely before the question of paying ever comes up. Pricing matches the site: Developer is free forever, Pro is priced per production application — the number comes from a call, and every paid seat starts with one — and Enterprise is custom.

Ecosystem

Anonymous usage telemetry

One heartbeat that says an install is alive. Eight fields, none of them yours, and one command that turns it off.

Not the same thing as Telemetry in. That is your monitoring data flowing into Drums so it can detect failures. This is a small usage signal flowing out, so we know how many installs are actually running.

The first time drums watch runs on a machine where it would send anything, it prints this in full, and prints it before the first heartbeat leaves. Once per machine, not once per run.

~/your-repo
$ drums watch
────────────────────────────────────────────────────────────
Anonymous usage telemetry is ON. You are seeing this once, on this machine.

Sent when `drums watch` starts, and every 6 hours while it runs:
  · a random install id, generated here, stored at
    ~/.drums/install-id
    (delete that file and you become a new install)
  · the drums version, and this machine's OS and CPU architecture
  · four running totals: failures detected, repairs attempted,
    repairs verified, repairs shipped

NEVER sent, absent from the payload, not redacted out of it:
  repository names · file paths · branch names · commit shas ·
  error messages · stack traces · request bodies · URLs ·
  agent output · failure-class names · anything derived from your code

To send nothing at all, either:
  export DRUMS_TELEMETRY=off
or add this line to ~/your-repo/.drums/config.toml:
  telemetry = "off"
────────────────────────────────────────────────────────────

The whole payload

FieldWhat it is
install_id128 random bits, generated once and stored in plain text at ~/.drums/install-id. Not derived from your hostname, MAC address, username, or repository path, nothing about the machine can reproduce it, two installs on one machine get different ids, and deleting the file makes you a new install.
drums_versionThe version of the binary.
os, archmacos/linux and aarch64/x86_64, compile-time constants of the binary, not an inspection of your machine.
failures_detectedA number. Never which, never where.
repairs_attemptedA number.
repairs_verifiedA number.
repairs_shippedA number.

Sent when drums watch starts and every six hours while it runs. It is a background send with a five-second timeout: it cannot delay or fail the loop, and a telemetry failure is never narrated as a product failure. drumsd, the detached daemon, sends nothing at all today.

What is never sent

Absent from the message, not redacted out of it, there is nowhere in it to put any of this:

Repository namesFile pathsBranch namesCommit shasError messagesStack tracesRequest bodiesURLsAgent outputFailure-class names

That list is enforced, not promised. One test in the engine pins the payload's field names to a literal list, so a ninth field cannot appear without someone deliberately editing it; a second feeds real repository paths, branch names, stack traces, error messages and request bodies through the counters and asserts none of them show up in the bytes on the wire.

Turning it off

Either one. The environment variable wins over the file.

terminal
$ export DRUMS_TELEMETRY=off
~/your-repo/.drums/config.toml
telemetry = "off"

The opt-out is enforced where the message is built, not merely where the notice is printed: an opted-out install generates no install id, starts no background task, and opens no connection. A value that is neither on nor off, or a .drums/config.toml that cannot be parsed at all, resolves to off, with a line on stderr saying so. An opt-out we could not read is never treated as consent.

Ecosystem

CLI reference

The whole product, scriptable.

drums initThe whole setup, one command: detect the framework, write the config, install the reporting snippet and the CI runner workflow (inert until team mode, and never written over a file that exists), and prove the path from your app to the record by sending a real event and reading it back. Prints the plan and waits before writing anything; --yes skips the prompt. Re-running is a no-op — except on an install from before the workflow existed, which is offered exactly that one file.
drums init --wireJust the reporting snippet, for re-running one part of setup.
drums init --verifyJust the round trip, against an already-running Drums. Earns a verified claim about your install, or says unresolved and exits non-zero so a setup script stops.
drums doctorWhy nothing is happening. Checks every precondition the loop depends on, the repository and its remote, the config, a coding agent on PATH, whether anything is listening on the ingest port, whether an error report or a deploy has ever arrived, and the account, then names the broken one and what to do about it. Read-only: starts nothing, writes nothing, fixes nothing. Exits non-zero when something is broken, so a setup script can stop; a fact this machine cannot establish is reported unknown and never fails the exit code. Worth running the moment Drums seems silent, because silence is also what a working loop prints when nothing has broken.
drums --versionThe release, the commit it was built from, and the target, e.g. drums 0.1.0 (037cf5f49a99, aarch64-apple-darwin). A build made from a modified tree says -dirty. Include it in anything you report.
drums openOpen a completed repair in your editor: a fresh worktree at the repair's commit, so your working tree is never touched. --close removes it.
drums loginConnect this machine to a Drums account. Prints a URL and a short code; you approve it in a browser, and no token is ever typed or pasted. --no-browser prints the URL instead of opening one. The credential lands in ~/.drums/credentials.toml, mode 0600, outside any repository, never in .drums/, which some teams commit.
drums logoutForget this machine's credential. Local only: the token stays valid until you revoke it in the console, which is what to do if the machine itself is no longer trusted.
drums whoamiWhich account this machine is signed in to. Prints the account and the console, never the token.
drums watchThe loop: detect, attribute, reproduce, repair, verify, propose. Live TUI on a real terminal; --plain (or DRUMS_PLAIN=1) forces the plain line-by-line narration. --propose-pr opens a pull request carrying the evidence. --repair-reported also attempts human-reported issues from Linear or Agentation, propose-only, and whether it resolves the report stays unresolved.
drums watch --dispatch-repairsRun the repair in your CI instead of on this machine, see team mode. Detection, attribution and reproduction still run locally: a failure that did not reproduce is never dispatched, because Drums only repairs failures it made happen again. Needs drums login and the repository connected at app.getdrums.app. Every dispatch asks a person first and prints the approval link; it stops asking only for a class that has earned act-alone and only with --repair auto. A console that is unreachable is narrated and the watch keeps running.
dashboardServed by the daemon itself at http://127.0.0.1:<ingest-port> while drums watch runs. Same origin as the API it reads, so there is nothing to deploy and no port to configure.
drums authorityWhat Drums may do on its own, per failure class. list shows each class's rung, streak and any promotion it has earned the right to propose; promote/demote are the only ways a rung ever changes by hand. Nothing promotes itself. promote is the one command in the product that needs a license, see act-alone is the paid capability.
drums activateStore a license key. Verified offline against a public key compiled into the binary, no license server is contacted, now or later. A malformed or expired key grants nothing, says exactly why, and changes nothing else: the free loop up to a verified, proposed repair never reads it.
drums digestThe morning message: what broke, what Drums did, what still needs you. Read-only, built from the record. --to slack posts it.
drums repairOne repair, from an instruction file to an outcome file. This is what runs in CI, it reproduces at the attributed revision, repairs, verifies, and writes what it found. The command itself never ships and never opens a pull request: the outcome file is the whole interface. What happens next is split on purpose, the workflow pushes the verified branch (a drums/ branch only, and only when the outcome says repaired), and the Drums App opens the pull request from it after checking the branch head is exactly the commit that was verified, so your own CI runs on the proposal and merging stays yours. Always writes an outcome, including when nothing was established, because a silent run is indistinguishable from one that never happened.
drums shipShip a proposed repair, then check that the original failure is gone.
drums revertRoll a shipped repair back to the deploy that preceded it.
drums recordThe append-only record, read back as history, newest first, every claim carrying its chip: verified, observed, inferred, approved, or unresolved.
drums hypothesizePropose a hypothesis: an interpretation of observed facts, citing at least one observation from the record by id, optionally carrying the evaluation plan a change would be measured against (--name --start --success --metric, plus guardrails and a window, whole or not at all). The judgment stays in your hands: Drums records it as inferred, marks the cited observations hypothesized, and refuses a citation the record does not hold. Without a plan the hypothesis records, and no change can proceed until one is attached.
drums hypothesis accept|rejectDecide on a proposed hypothesis. Accepting is what lets a change cite it; rejecting requires the reason, which is recorded beside it and quoted back if anyone tries to re-decide.
drums changeRecord a shipped change against an accepted hypothesis. Freezes the evaluation plan and the baseline reading at that moment, the plan you ship under is the plan you are measured under. Reads the baseline from whichever source measures the plan's metric, error_event_rate from the record, completion_rate/abandonment from your PostHog, and refuses, naming the missing configuration or source, when none can. The outcome is measured by drums watch once the window fully elapses, written beside the change, with any guardrail the record could not read listed as unread.
drums bet createPre-register a Product Bet: what you believe (--belief), why (--because, plus --cited observations), who it should affect (--for), roads not taken (--alt), and the expectation, --name --start --success --metric with optional guardrails and window, and here the expectation is not optional: a pre-registration that does not say what it expects is a diary entry, not a bet. Writes the bet and the hypothesis under it; nothing is committed to until confirmed.
drums bet confirm|declineConfirm is the pre-registration moment: the belief goes on record, timestamped, before the outcome exists, and the hypothesis underneath is accepted so a change can proceed. Decline records the reason and keeps the bet, a decision not to act is a decision too.
drums bet learnRecord what the team learned from an evaluated (or declined) bet, refused before an outcome exists, because before the outcome a "learning" is a prediction wearing the wrong name.
drums bet showOne bet as its full card, belief, evidence, expectation, verdict with its causal confidence, learnings, and any prior evaluated bets on the same metric, or every bet, newest first.
drums draftDrums drafts a Product Bet from the record (unclaimed observations, the memory of evaluated bets, open bets) using the coding agent you already use, in plain prompt mode with no edit permissions. The draft is validated exactly like a human's drums bet create (a hallucinated citation dies at the same wall a typo would) and recorded as proposed; nothing is committed to until you confirm. The agent may decline to draft, and its reason is shown verbatim, a skip is a good outcome.
drums indexRebuild the semantic index: every record object as a node in a typed graph with searchable text, in SQLite inside .drums/. Derived and disposable, the record stays the only source of truth. Embeddings are optional (an OpenAI-compatible endpoint you name by environment; the key never touches a config file) and cached by content hash.
drums askAsk a question over the record in plain words. Lexical search always works with zero configuration; vector search joins in when an embedding endpoint is configured, and the answer says which one ran, lexical-only is stated, never silently degraded.
drums syncPush this repo's record to the hosted plane, once, by hand. Opt-in per repo: sync_record = true in .drums/config.toml, default off, your record leaves this machine only when you set it, and it arrives redacted because it is stored redacted (the wire carries the stored bytes; sync cannot add a field). With the flag set, the daemon syncs on its tick and the team reads the Bet Feed at app.getdrums.app.
drums digest --sendDeliver the digest to Slack as one notification (slack_webhook_url in config, or DRUMS_SLACK_WEBHOOK_URL): an FYI when nothing needs anyone, a Decision with the count when something does. Refuses, naming the key, when no webhook is configured.
drums whyA file's production history: what broke there, when, and whether it was proven.
drums daemonRun the loop as a background service, so it keeps watching when the terminal closes.
drums routineIn design Describe scheduled maintenance in a sentence, not in the CLI yet.
ctrl-c / SIGTERMStops drums watch immediately: worktrees are cleaned up and in-flight agent process groups are killed.
DRUMS_TELEMETRY=offSends no anonymous usage telemetry at all, no install id is generated, no background task starts, no connection is opened. telemetry = "off" in .drums/config.toml does the same; the variable wins over the file.