-
-
deadhand: a dead man's switch for software. When maintainers go silent, GitLab Duo agents keep the project alive.
-
What only deadhand does: it acts only while maintainers are silent, finishes what the community started, and hands the wheel back.
-
How it works: one hands-off loop across all nine DevSecOps stages. Nothing ships on one model's word.
-
The live Logbook, 26 October: every number is computed from the hash-chained ledger; docs/numbers.py recomputes them all.
-
Two keys on a real denial-of-service fix: code gates plus an independent witness, sealed to one commit. Merged in 12 minutes, hands-off.
-
The witness refusing to sign deadhand's own first change, because it was asked before the build finished. It was right.
-
9 to 26 October: a 2020 bug live in 2h 22m with 0 humans, 12 community MRs landed and credited, 29 merged with both keys.
-
The dead man's canary on Cloud Run: a new revision answers first, then takes 10% and must keep proving it's healthy, or it lets go.
-
A second patient: Regexper, JavaScript, abandoned in 2020. The same public flows, with its own mandate, AGENTS.md and a few CI settings.
-
Fire drill: a bug report carrying a prompt injection. deadhand ignored the instruction, named it, and parked the issue.
See it running: Logbook (live status computed from the ledger) · live app on Cloud Run · repository · for judges: one page · deadhand-live, from 27 October 13:00 UTC · second patient · evidence for every claim
In one minute
In April 2020 a Commento user reported that the dashboard's statistics count deleted comments. Another user confirmed it three days later. Nobody fixed it: Commento's maintainer had stopped committing by February 2021. On 19 October 2026 deadhand adopted the report, and 2 hours 22 minutes later the fix was live. Nobody touched it: the ledger records no human action between the report and the release.
In that time a Duo flow wrote the fix and a test that fails on the parent commit and passes on the fix; a separate witness flow signed a verdict bound to that exact commit; and a canary on Cloud Run held 63 probes at 10% of traffic with 0% errors before taking the rest. That one run used GitLab features from seven of the nine DevSecOps stages, and deadhand uses all nine (the receipts, with dates).
What only deadhand does
- It acts for a maintainer who is gone, under limits they wrote, for a fixed term. A mandate (
DEADHAND.md) lists its powers, what it may never touch (logins, migrations, its own gates) and when its term ends. Code enforces it, not a model, and no person approves each change. When a fix needs a path it may not touch, it parks the issue and names the clause. - It hands the wheel back. It acts only while the maintainers stay silent: a sign of life from one of them, or one label, and it goes back to watching. When its term ends it stands down by itself: schedules off, a last entry in the ledger, the Logbook says so. At 10:25 UTC on 27 October it stands down in the submitted repository, and a separate copy, deadhand-live, keeps working under the same mandate through judging.
- It finishes what the community started. 12 stranded merge requests landed with their authors on the commits, one after 7 years 4 months. 36 reports adopted from the abandoned upstream, 6 fixed and live, each crediting its reporter.
Why it's safe to leave alone
- No change merges on one model's word. Key 1 is code: the mandate, the tests, the scanners, and proof that the new test fails on the parent and passes on the fix. Key 2 is a separate witness flow whose verdict is bound to the exact commit. The witness refused deadhand's very first change, and it was right.
- Production has to keep proving it's healthy. Each release takes 10% of Cloud Run traffic through a five-minute hold; a failure rolls back and opens a GitLab incident.
- The keys bind the steward too. Project code that reaches the default branch without both keys trips a wire: an incident, deadhand's own kill switch, no deploy. The ledger counts every human action.
Final (27 October, 10:35 UTC, when deadhand stood down; docs/numbers.py in the repository recomputes every number from public data, no account needed): 29 changes merged with both keys, all 29 live · 0 reachable vulnerabilities · 12 credited community contributions · 36 upstream reports adopted, 6 fixed · 54 issues parked with the clause that stopped them · 12 canary rollbacks, 3 of them drills · 18 hands-on human actions on the project, every one in the ledger with its reason.
Inspiration
Code generation is solved. What kills software is what happens after its people leave.
Commento is a privacy-focused comments platform hosted on GitLab. Its maintainer's last commit was 28 February 2021. Thirty-five community merge requests were never merged, the oldest from January 2019. In March 2025 its hosted service went dark while users were still being billed. One of them wrote: "Not only is this project abandoned on Gitlab, but the Commento cloud service is now derelict too."
That's not unusual. About 60% of open-source maintainers have quit or considered quitting (Tidelift, 2024), and only about 11% of projects are actively maintained (Sonatype, 2023). Most software doesn't die from bad code. It dies when nobody's at the wheel.
Commento is the test case, not the point. Comentario, an actively maintained Commento fork, has been rewritten until no Commento code is left, and it imports Commento's data: if you want a comment server, use it. Most abandoned projects never get a rewrite (Commento++, an earlier fork, has had no commits since October 2022), and deadhand is for those. Commento made an honest test because nothing about it was staged: real vulnerabilities, a build that no longer ran, and 35 merge requests nobody merged. deadhand's flows don't name it, and the same flows keep a second, unrelated project alive.
What it does
A steward writes a mandate (DEADHAND.md) once: what deadhand may fix on its own, what it may never touch, and when its authority expires. The steward is whoever can't let the project die: a maintainer before a long leave, a company whose product runs on it, or a foundation that holds it. When the maintainers' pulse goes silent, the switch releases and deadhand takes the wheel:
- Intake reads new issues as untrusted data. Spam never reaches a model. Adopt files the abandoned upstream's open bug reports here, crediting their reporters, and they go through the same loop.
- A Duo flow triages the issue against the mandate and writes its plan on the issue, then mends it with a regression test and opens a merge request. Or it parks the issue with one note that names the clause that stopped it.
- Two keys guard every merge. Key 1 is code: the mandate checker, proof-of-fix (the new test must fail, or fail to compile, on the parent commit and pass on the change), tests and scanners. Key 2 is a witness: a separate flow with its own context and read-only tools, asked only after key 1 has passed, that signs a verdict bound to the exact commit.
- Releases go out as a dead man's canary on Cloud Run: a new revision must answer on its own URL, then take 10% of traffic and keep proving it's healthy. Errors, slowness or silence roll it back and open a GitLab incident.
- deadhand revives stranded community merge requests with their original authors, and ports the ones whose patches no longer apply, crediting the authors.
- Every decision lands in a hash-chained ledger. The public Logbook and a "while you were gone" debrief are computed from it.
- A medic gets a fixed number of tries to repair a merge request that breaks the build; a verdict (the mandate refused, the witness withheld) is never argued with. A tripwire stops deadhand if project code reaches the default branch without both keys.
- Any maintainer takes the wheel back by adding one label,
deadhand-hold, to an issue, from a laptop or a phone. And when the term ends, deadhand stands down on its own: it switches every schedule off and records the stand-down, which is what a successor checks before it takes over.
Its first act, on waking, was automatic: CI started an autopsy of Commento with no one asking. It checked the steward's notes against the code (why the build died, what attackers could still reach, a real bug in its shutdown code) and filed an ordered resurrection plan. Less than five minutes after intake picked up step one, deadhand had a merge request that made Commento's 2021 pipeline build again. Then the witness refused to sign it, because deadhand had asked before the build finished. It was right. We fixed deadhand, gave the change a fresh hearing, and it merged with both keys. Later that day the witness refused again: on the dependency upgrade that takes Commento from 18 reachable vulnerabilities to 0, deadhand's description listed modules that weren't in the diff, and its build script downloaded a compiler without checking it. Right again. Meanwhile a real denial of service went from pickup to merge in twelve minutes with nobody touching it.
Nine stages, with receipts
A stage counts when the automation uses a GitLab feature that belongs to it. Issue #46's run, from adopted report to live fix on 19 October, used seven. The other two joined later, and their receipts carry their dates:
- Plan: issues and labels.
adoptfiled #46; the mend flow's triage note took it under thefix-bugpower. - Create: the Duo Agent Platform. The mend flow wrote the fix and its test on a branch and opened merge request !29.
- Verify: CI/CD pipelines. Commento's own jobs, then proof-of-fix: the new test fails on the parent and passes on the fix.
- Secure: SAST, Secret Detection and Dependency Scanning on the merge request, and govulncheck as a ratchet measured with the Go that ships.
- Govern: a scan execution policy in a separate project the agents can't edit, from 24 October until the freeze (deadhand-live keeps it). On 25 October the policy's own pipeline ran the scanners on the default branch; on Regexper it ran a day earlier. In #46's run, deadhand's own merge gates: the mandate check, the witness's verdict on the exact commit, both keys, then auto-merge pinned to that commit; the tripwire accounts for every push to the default branch.
- Package: the generic package registry. The CLI, and every ledger entry of the run.
- Configure: environments with deployment tiers, environment-scoped protected variables, and OIDC ID tokens: the canary deployed to the
productionenvironment through Workload Identity Federation, with no stored cloud key. - Release: GitLab Releases and Pages. The fix became release v2026.10.19-b0d7ec42, and the Logbook rebuilt on Pages.
- Monitor: incident management. In #46's run the canary held five minutes at 10% of traffic, so there was nothing to open. Since 24 October a canary that lets go opens a GitLab incident: on 25 October a drill release built never to start got no traffic, and the canary filed incident #79 by itself, then closed it because it was a drill.
The README links the job, note, deployment or release for each one.
Results so far
All of these come from the ledger behind the live Logbook, where every entry links to its merge request, pipeline or issue, and docs/numbers.py recomputes them.
- 29 changes merged with both keys, all 29 live in production through the canary. The fastest went from pickup to live in 22 minutes; the median over 22 changes is 53 minutes, and from a report being filed to its fix going live, 2.9 hours.
- 12 stranded community contributions landed, credited to their authors, the longest after 7 years 4 months. Five were ports: deadhand re-applied the author's change to today's code and credited them with a Co-authored-by trailer.
- 36 upstream reports adopted from the abandoned upstream, 6 fixed and live: outdated Go dependencies, the statistics bug above, client IP handling, umlauts in mail, data-export email over SMTP AUTH LOGIN, and a reply-count tooltip. The rest are parked with the clause that stopped them, most because the bug couldn't be reproduced in today's code, so no test could fail before a fix.
- Reachable vulnerabilities: 0, measured with the Go the image builds with. Commento started with 18 in its dependencies, and 37 more in its Go standard library that we found on day two. When a new Go advisory made one reachable again (22 October), deadhand shipped Go 1.27.3 on 24 October.
- 12 canary rollbacks: three revisions that failed to start on day two, four false alarms that each became a fix, three drills built to fail, and two releases that a drill's leftover setting stopped from starting (see Challenges).
- 54 issues parked for a human, each with one note saying why.
- 7 of 7 fire drills passed, 0 unsafe (prompt injection, the auth surface, a major upgrade, a feature request, spam, a report too thin to reproduce, and a real bug). Drill issues look like ordinary reports.
- About 33 gCO2e per merged change (Software Carbon Intensity over nine days on both projects, 21 to 45 g as inference-energy assumptions vary). Tokens and runtimes are measured; energy and emissions are modelled. Method and per-session data:
docs/SUSTAINABILITY.md. - 18 hands-on human actions on the project after the mandate was signed, and 0 tripwires. Each is in the ledger with its reason: fresh hearings after the witness caught flaws in deadhand itself, re-queues, issues the steward filed (a new Go advisory, a broken build), build re-runs, one rebase, and closing the two incidents a drill caused. The 118 changes to deadhand's own harness are counted separately, also in the ledger.
A second patient: Regexper
To show the agents revive abandoned code rather than one hand-picked repo, deadhand took on a second patient: a fork of Regexper (regexper.com, a JavaScript regex visualizer whose author's last commit was in September 2020, with no maintained successor). The same four public flows, pointed at it with a new mandate, an AGENTS.md and a few CI settings, ran their own autopsy and planned six resurrections. Two merged hands-off and its build runs again. The witness withheld two others for going beyond their issue (one tried to bring in about 40 major-version upgrades the mandate forbids), and the medic refused to merge a good fix it couldn't prove against a flaky test suite, filing the flaky spec instead. The autopsy also found a live cross-site-scripting bug on regexper.com. We disclosed it privately to Regexper's author and kept the report confidential.
How we built it
- GitLab Duo Agent Platform: four custom flows published openly to the AI Catalog from the repository (
deadhand-mend: a triage agent routed to a mend agent;deadhand-witness;deadhand-autopsy;deadhand-medic), started through the Flows API, with GraphQL session status, composite identity and service accounts,AGENTS.md,agent-config.yml(image, setup, network policy), tool governance (the witness only gets read-only tools), and every session's model recorded (Claude Sonnet 5.5 on Google Cloud Vertex AI). The same catalog flows run on both projects. - The constraint that shaped it: GitLab lets only humans fire flow triggers, so a flow can't wake another flow. deadhand makes CI the spine. CI starts each flow through the Flows API with the steward's scoped token, which expires the day after the mandate's term. GitLab's composite identity carries the steward's identity into every agent action: the absent owner's authority, bounded by a written mandate. That's literally a dead hand.
- GitLab CI/CD and the rest of the platform: merge request pipelines, stage-ordered keys,
needs,resource_group, OIDCid_tokens, environments with environment-scoped credentials, schedules chosen by description; SAST, Secret Detection and Dependency Scanning on every merge request, plus a scan execution policy;govulncheckas a ratchet; auto-merge pinned to a SHA; Releases; incidents; the Generic Package Registry (the ledger and the CLI); GitLab Pages (the Logbook, built on Pajamas design tokens). - Google Cloud: Cloud Run (tagged revisions and traffic splitting for the canary, scale to zero), Cloud SQL for PostgreSQL, Cloud Build, Artifact Registry, Secret Manager, keyless deploys through Workload Identity Federation, Budgets.
- The
deadhandCLI (Go, tested package by package) enforces everything deterministically: the mandate, proof-of-fix, the keys, the canary, the pulse, intake, adopt, revive, the ledger, the debrief and the Logbook.
What we added during the Submission Period (Path B)
Everything except Commento's own history: the mandate, the flows, the CI spine and security gate, the CLI, the Google Cloud deployment, the Logbook, and every change to Commento's code. Each of those changes was made or landed by deadhand, crediting original authors (see git log on api/, frontend/, .gitlab/ci/commento.yml).
Challenges we ran into
- Flows can't trigger flows, so we built the loop around CI and waited for each flow's output (a merge request, or a verdict note bound to a SHA): a session's status says it ended, not that it did the job.
- Our own witness refused us, more than once. On the first merge request deadhand asked for key 2 while Commento's build was still running, and the witness wouldn't sign an unproven pipeline repair. It was right; key 2 is now asked for only after key 1 has fully turned. On the dependency upgrade it caught deadhand's own description claiming modules the diff didn't contain, and an unpinned toolchain download. Now the mender describes only what its diff shows and pins every download to the vendor's checksum.
- We measured the wrong thing once, and the ledger says so. An independent review of our claims found the security gate measured with the CI image's Go while the app was built with Go 1.25.3, whose standard library had 37 reachable vulnerabilities the gate couldn't see. The gate now measures with the Go the Dockerfile names, and deadhand moved Commento to Go 1.27.2 itself, twelve minutes from pickup to merge.
- The day the build broke (24 October). A download from GitHub that Commento's frontend build relies on stopped answering GitLab's runners, and every merge request's Docker build stalled for an hour. The steward filed it; deadhand parked it, because it couldn't prove a root cause and the mandate says park when unsure. The steward proved a fix on a test branch but didn't merge it: a steward merge of project code trips deadhand's own tripwire. They put the evidence on the issue instead, and deadhand made the change itself through both keys and the canary, live 2.9 hours after the report. Three human steps, all in the ledger. The Go 1.27.3 upgrade waiting behind it took 8.4 hours and four human steps for the same reason, and we count them.
- Being quiet is hard. deadhand's first revive run credited upstream authors with @-mentions and linked their merge requests, so GitLab notified them and posted "mentioned in" notes on 15 of Commento's merge requests and one issue. We paused it, built
internal/quiet(profile links, code-span citations, quoted text and commit messages quieted), and wrote it in the ledger. It wasn't the last slip: GitLab reads titles, commit messages and HTML without Markdown, so code spans didn't quiet those, and four more notes reached upstream. An independent review caught them on 19 October. Those places now use look-alike characters that GitLab can't read as references, and the ledger records that too. - The canary once rolled back a README-only release because one cold start made p95 look slow after six samples. p95 now needs 20 healthy samples; errors still count from the first six.
- It also rolled back a docs-only release whose new revision URL answered HTTP 500 for 16 seconds. Cloud Run's request log showed none of those probes had reached the revision: Google's front end hadn't routed the new URL yet. Now a revision has to answer on its own URL before it gets any traffic, which also means no user is ever sent to a revision that hasn't answered once.
- Twice more it let go of a healthy release over one 404, a minute into the hold. The request log showed the same front end answering for a revision that never saw the request. Now, before the hold ends, a rollback takes two failed probes; at the end, the error rate alone decides, as the mandate says.
- One of our drills broke two real releases (25 October). The drill deploys a release built never to start. Cloud Run keeps a deploy's settings for the next one, so the next two releases inherited the bad port and never started either. The canary did its job: neither got any traffic, each opened a GitLab incident, and users stayed on the healthy revision. Every release now copies the settings drills change from the revision it replaces, and the steward closed both incidents with the root cause.
- Our witness held back three good ports because of a bug of ours. It must check that a port credits its author with a Co-authored-by trailer, but its tools can't read commit messages, so it played safe. CI now quotes the commit messages in its brief; the steward gave the stuck ports a fresh hearing, and each one is a counted human action.
- Handing over at the deadline is its own problem. The rules freeze the submitted repository at the deadline, so deadhand stands down there and a copy takes over. An audit of our own handover found the last Logbook build raced the stand-down, so the copy would never have seen it, and that work picked up in the last hour could land after the freeze. The Logbook now waits for the stand-down, the stand-down fails loudly, and it runs at 10:25 UTC, early enough for anything in flight to land first.
- The hackathon workspace gives participants the Developer role: no branch protection, no protected environments, no project access tokens, no pipeline variables. We set everything at creation, scoped the steward's token to two environments so agent sessions never see it, chose tasks by schedule description, added a tripwire for what we couldn't enforce, and wrote down the rest.
- Honesty about security:
govulncheckfound 18 reachable vulnerabilities, so the gate became a ratchet (no change may add one) rather than a wall that blocks the fixes. The worst was a real denial of service in page-title fetching. A famous XSS in Commento's sanitizer turned out not to be exploitable at the version it pins, and we say so.
Accomplishments that we're proud of
- A real bug report from 2020, fixed and live in 2 hours 22 minutes with no human action, and every step of it public and checkable.
- Twelve community contributions finally landed, years late and credited to the people who wrote them; six of the abandoned upstream's bug reports fixed, crediting their reporters.
- A hands-off loop whose claims you can check: commit authorship, a hash-chained ledger, a human-action count computed from it, a tripwire that binds the steward too, fire drills (7/7 passed, 0 unsafe), and a script that recomputes every number from public data.
- The same public flows keeping two unrelated abandoned projects alive, in two languages.
- Fixing what we hit along the way, upstream: two GitLab documentation merge requests from gaps we ran into while building deadhand. One, on which
pages_access_levelvalues each project visibility allows, is merged into GitLab; the other, on how to check a flow's status, is approved. We also reported an over-broad Go vulnerability advisory to the Go vulnerability database and GitHub's advisory database.
What we learned
The hard part of autonomy isn't writing code. It's knowing what you're allowed to change, proving a fix is real, and letting go of a release that isn't healthy. Those parts belong in code, not in a prompt. And a second key is only worth having if it can say no to you: the best moment of the build was the witness refusing to sign deadhand's own first change.
What's next
deadhand-live carries on through judging: from 13:00 UTC on 27 October, file an issue there and watch it handled. Then point deadhand at the next project whose people moved on. Its flows are already public in the AI Catalog and don't name Commento, so adopting it means writing a mandate and an AGENTS.md and setting a few CI variables (the README has the steps). After that: a mandate template, and succession, handing the wheel to a new maintainer the pulse finds.
Built With
- artifact-registry
- claude
- cloud-build
- cloud-sql
- gitlab
- gitlab-ci
- gitlab-duo-agent-platform
- gitlab-pages
- go
- google-cloud
- google-cloud-run
- javascript
- postgresql
- vertex-ai
- workload-identity-federation
Log in or sign up for Devpost to join the conversation.