{"id":51530694,"url":"https://github.com/acoyfellow/deadlint","last_synced_at":"2026-07-09T02:01:05.435Z","repository":{"id":356101268,"uuid":"1231044170","full_name":"acoyfellow/deadlint","owner":"acoyfellow","description":"Find dead cross-boundary methods (Workers DurableObject / RpcTarget / Agent) and structural clones in TypeScript. The linter layer that sits above knip/oxlint.","archived":false,"fork":false,"pushed_at":"2026-05-06T17:16:49.000Z","size":1114,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-06T17:31:08.542Z","etag":null,"topics":["cloudflare-workers","dead-code","developer-tools","duplicate-code","durable-objects","lint","static-analysis","typescript"],"latest_commit_sha":null,"homepage":"https://github.com/acoyfellow/deadlint","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/acoyfellow.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T15:12:15.000Z","updated_at":"2026-05-06T17:17:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/acoyfellow/deadlint","commit_stats":null,"previous_names":["acoyfellow/deadlint"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/acoyfellow/deadlint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acoyfellow%2Fdeadlint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acoyfellow%2Fdeadlint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acoyfellow%2Fdeadlint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acoyfellow%2Fdeadlint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/acoyfellow","download_url":"https://codeload.github.com/acoyfellow/deadlint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acoyfellow%2Fdeadlint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35283905,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare-workers","dead-code","developer-tools","duplicate-code","durable-objects","lint","static-analysis","typescript"],"created_at":"2026-07-09T02:01:04.653Z","updated_at":"2026-07-09T02:01:05.428Z","avatar_url":"https://github.com/acoyfellow.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./docs/img/banner.jpg\" alt=\"three skulls and ritual ornaments, block-printed in oxblood ink on cream paper\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003edeadlint\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\u003cem\u003eRuthlessly Eliminate the Dead.\u003c/em\u003e\u003c/p\u003e\n\n---\n\nFind dead public methods on Cloudflare Workers `DurableObject` /\n`WorkerEntrypoint` / `WorkflowEntrypoint` / `RpcTarget` / `Agent` subclasses,\nand structural clones across your TypeScript codebase.\n\n## The gap\n\n| Tool                         | Stops at                | Result                                            |\n| ---------------------------- | ----------------------- | ------------------------------------------------- |\n| `tsc` (`noUnusedLocals`)     | function boundary       | unused vars only                                  |\n| `oxlint` / `biome` / `eslint`| file/class boundary     | every `export` is treated as live                 |\n| `knip` / `ts-prune`          | module-export boundary  | every public class member is treated as the API  |\n| **deadlint**                 | RPC stub / clone        | this is the layer that was missing                |\n\nIn a Workers / Agents codebase, every public method on a DO is — to a static\nanalyzer — an entry point. Anyone with a stub could call it, so nothing dares\nflag it. Real codebases accumulate dead RPC methods for years and no linter\nwill tell you.\n\ndeadlint walks the call graph plus a targeted token scan and tells you which\nones are actually unreachable.\n\n## What it found, on real repos\n\n```text\n$ npx deadlint ./artifacts\ndeadlint report — ./artifacts\n\nDead RPC methods (1)\n───────────────────────────\n  GitServer.streamingUploadPack  (extends DurableObject)\n    apps/worker/src/git-server.ts:299\n\nClones — engine: inline (3)\n───────────────────────────\n  1.00  27L  readBlob ≈ readTree\n    src/capabilities/repo.ts:135\n    src/capabilities/repo.ts:167\n  1.00  27L  readBlob ≈ readCommit\n    src/capabilities/repo.ts:135\n    src/capabilities/repo.ts:199\n  1.00  27L  readTree ≈ readCommit\n    src/capabilities/repo.ts:167\n    src/capabilities/repo.ts:199\n\n4 findings\n```\n\n`streamingUploadPack` was a wrapper around `streamingUploadPackWithEventType`\nthat nothing called for months. `readBlob` / `readTree` / `readCommit` were\n27-line copy-pastes of each other. No other linter saw any of it.\n\n## Install and run\n\n```bash\n# one-off\nnpx deadlint /path/to/your/repo\n\n# global\nnpm i -g deadlint\ndeadlint /path/to/your/repo\n```\n\nThe path needs a `tsconfig.json` somewhere — at the root, in `apps/worker/`,\nor anywhere within the first 3 directory levels. Multi-package monorepos\nwithout a root config (a `packages/api/tsconfig.json` + `packages/web/tsconfig.json`\nlayout) are auto-discovered and scanned per-project.\n\n```bash\ndeadlint ./repo                              # full scan (default)\ndeadlint ./repo --check dead-rpc             # just the dead methods\ndeadlint ./repo --check clones               # just the clones\ndeadlint ./repo --clones-engine both         # similarity-ts + inline engine\ndeadlint ./repo --json \u003e findings.json       # machine-readable\ndeadlint --help                              # all flags\n```\n\nExit `0` = clean, `1` = findings, `2` = misconfig.\n\n## What it actually checks\n\n**Dead RPC methods.** For every public method on a class extending\n`DurableObject`, `WorkerEntrypoint`, `WorkflowEntrypoint`, `RpcTarget`,\nor `Agent`, deadlint looks for callers across three signals:\n\n1. The TypeScript language service (precise — but blind to JSRPC stubs).\n2. A token scan for `.method(` / `[\"method\"](` direct dispatch.\n3. A token scan for `.call(\"method\", …)` string-key dispatch — the\n   Agents SDK pattern frontend code uses to reach DO methods through\n   the WebSocket proxy.\n\nPatterns 2 and 3 are scanned across both TypeScript files and companion\nfiles (`.svelte`, `.vue`, `.astro`, `.tsx`, `.jsx`) so frontend call\nsites that aren't compiled by your tsconfig are still seen. A method is\nflagged dead **only when all three signals turn up zero**. Common\nstdlib names (`map`, `then`, `set`, …) are excluded from the token\nscans to avoid coincidental keep-alives. Workers/Agents runtime hooks\n(`fetch`, `alarm`, `onConnect`, …) are allow-listed.\n\nOverride the boundary class list with `--bases Foo,Bar`.\n\n**Structural clones.** Two engines, run independently, results merged.\n\n`similarity` (default) shells out to\n[`similarity-ts`](https://github.com/mizchi/similarity) — a Rust binary\nusing oxc-parser and TSED. Higher precision, biased toward larger\nfunctions. Install once: `cargo install similarity-ts`.\n\n`inline` is built into deadlint, ~150 lines of `ts-morph`. Each function\nbody is normalized to a `SyntaxKind`-only token sequence (identifiers\nand literals erased, so renamed copies match exactly). Findings are\nemitted on identical shapes (`1.00`) or 5-gram Jaccard similarity above\n`--clone-threshold` (default `0.85`). No external dependencies.\n\nUse `--clones-engine both` to run them side by side. They find largely\nnon-overlapping pairs.\n\n## Always on, every repo\n\n```bash\ndeadlint --install-hook\n```\n\nSets `git config --global core.hooksPath ~/.config/git/hooks` and writes\na `pre-push` script that runs `deadlint . --check dead-rpc` before every\npush. Covers every repo on your machine — public, private, GitHub, GitLab.\nThe hook silently no-ops on non-TypeScript repos.\n\n```bash\ndeadlint --hook-status      # is it installed?\ndeadlint --uninstall-hook   # remove it (only if we wrote it)\ngit push --no-verify        # bypass once\n```\n\nThe installer refuses to clobber a pre-existing `pre-push` hook unless\nyou pass `--force`. The uninstaller refuses to remove anything that\nisn't deadlint-managed. You can't accidentally lose work.\n\n## What it won't catch\n\n- Fully dynamic RPC: `stub[methodFromConfig]()` where the method name is\n  computed at runtime. None of the signals can see it.\n- Cross-repo dead code. If your callers live in a different repository,\n  deadlint sees nothing.\n- HTTP routes dispatched by URL path rather than method name. Add the\n  router class to `--bases` if appropriate.\n- Behavioral clones with different control flow. The inline engine is\n  shape-based; the similarity engine helps but isn't magic.\n\nBuild-output directories (`dist`, `build`, `.svelte-kit`, `.next`, etc.)\nare excluded by default — they generate ~100% clone matches against the\nsource they were built from. Pass `--exclude` to replace the list, or\n`--also-exclude` to extend it. Note: `lib` is **not** in the default\nlist because SvelteKit and many other frameworks use `src/lib/` for\nsource code; pass `--also-exclude lib` if your project emits to it.\n\nFindings are meant for human review. The tool biases toward false\nnegatives — it would rather miss a dead method than wrongly flag a live one.\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Facoyfellow%2Fdeadlint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Facoyfellow%2Fdeadlint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Facoyfellow%2Fdeadlint/lists"}