cd /news/ai-tools/loose-ends-a-check-for-unused-code-a… · home › topics › ai-tools › article
[ARTICLE · art-168780] src=github.com ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Loose Ends: A check for unused code AI wrote or forgot to remove

A new open-source tool called Loose Ends, published to npm as loose-ends and requiring Node.js 20 or newer plus git, checks AI-written code changes before merge and reports whether added or touched code is still reachable from a real entry point. Loose Ends builds a Liveness Graph across TypeScript, JavaScript, Python and Java — covering imports, exports, re-exports, top-level references, dynamic imports and package dependencies — and flags two error classes, orphaned-addition and superseded, as shown in its express-api example where an agent added a v2 service, left the original behind and added an unused pagination helper, producing 2 errors and a failed check. The tool's bench/RESULTS.md reports results on public repositories, including about 700 Python and Java commits replayed through check and a sweep of 30 popular repositories, and it never deletes code without deterministic evidence and a passing verification run.

read7 min views3 publishedNov 13, 2026
Loose Ends: A check for unused code AI wrote or forgot to remove
Image: Michielbdejong (auto-discovered)

Graph-powered liveness checks for AI-written code.

Every change should lead somewhere.

Coding agents write code fast, and they leave things behind: a helper that is never called, a UserServiceV2 sitting next to the UserService it replaced, a route that was unregistered while its handler stayed. Loose Ends checks every change before merge and reports, with evidence, whether the code it added or touched is still reachable from a real entry point.

$ npx loose-ends check
Loose Ends · check main (dc9af08) → working tree
4 files changed

✖ error   orphaned-addition  src/lib/pagination.ts:1
  File added in this change is not reachable from any entry point.
  Import it from live code, add it to `entries`, or mark it with `// loose-ends-file: dormant <reason>`.

✖ error   superseded  src/services/users.ts:1
  src/services/users.ts appears to have been replaced by src/services/users-v2.ts#listUsersV2 but was not removed: its callers now use the new code (src/routes/users.ts); the names match; 100% structural similarity. Delete it, or mark it dormant with a reason.
  edge removed: src/routes/users.ts → src/services/users.ts
  was reached via: src/server.ts → src/routes/index.ts → src/routes/users.ts → src/services/users.ts
  root: entry point "src/server.ts" of express-api-example
  related: src/services/users-v2.ts#listUsersV2

Summary: 2 errors, 0 warnings, 0 info
Files: 6 live, 0 test-only, 2 orphaned, 0 uncertain, 0 dormant
Result: fail (2 findings in failOn: orphaned-addition, superseded)

That is the express-api example after an agent-style change: a v2 service was added and wired in, the original was left behind, and an unused pagination helper was added.

  1. Added code must be reachable from a recognized product entry point.
  2. Changes must not orphan existing code by removing the edges that kept it alive.
  3. Replaced implementations must be removed or explicitly justified.

Loose Ends builds a Liveness Graph of TypeScript, JavaScript, Python, and Java code: imports, exports, re-exports, references between top-level declarations, dynamic imports, and package dependencies. It finds roots in package manifests, scripts, framework conventions (Next.js routes, Vite entries, browser extension manifests, serverless functions, tool configs, Django apps, pyproject entry points, server commands in Dockerfiles and Procfiles, Spring and Jakarta annotations, Android manifests and layouts), config, and tests. It analyzes the base and head revisions and reports only what the change did. Existing dead code never blocks a pull request.

It is not an LLM guessing whether code looks unused, a general code reviewer, a formatter, or a security scanner. It does not claim that unobserved code is safe to delete, and it never deletes anything without deterministic evidence and a passing verification run.

bench/RESULTS.md has the results on public repositories, including about 700 Python and Java commits replayed through check and a sweep of 30 popular repositories.

npm install --save-dev loose-ends

Requires Node.js 20 or newer and git.

Python and Java projects need no package.json. With Node installed, run npx loose-ends check from the repository root.

The npm packages are published from GitHub Actions with provenance, so each version links back to the commit it was built from.

npx loose-ends check                     # working tree vs. merge base with the default branch
npx loose-ends check --base origin/main --head HEAD
npx loose-ends check --format markdown   # also: json, sarif
npx loose-ends scan                      # whole-project report of existing debt
npx loose-ends explain src/lib/format.ts#formatUser
npx loose-ends roots                     # what counts as an entry point, and why
npx loose-ends fix --verify --patch cleanup.diff
npx loose-ends init                      # starter loose-ends.config.json

check exits with 0 on pass, 1 on blocking findings, and 2 when it could not run.

Kind Severity Meaning
orphaned-addition error Added code that no production root reaches.
newly-orphaned error Code that was live before the change and is not live after it.
superseded error A replaced implementation that was not removed.
invalid-suppression error A suppression or dormant marker that has no reason, has expired, or matches nothing.
test-only-addition warning Added code that only tests reach.
newly-test-only warning Production stopped using this code, and only tests still do.
duplicate warning Added code that is near-identical to an existing live implementation.
unused-dependency warning A dependency that no live code imports.
uncertain info Reachability depends on a dynamic import the graph cannot resolve.

scan reports existing code instead of a change, so it uses orphaned (nothing reaches it) and test-only (only tests reach it), plus unused-dependency and uncertain. It exits 0 unless you pass --fail.

See docs/findings.md for what each kind means and what to do about it.

Every option is optional. npx loose-ends init writes a starter loose-ends.config.json:

{
  "$schema": "https://raw.githubusercontent.com/sid732/loose-ends/main/schema/config.schema.json",
  "entries": ["src/jobs/*.ts", "src/events/handlers.ts#onInvoicePaid"],
  "extendExclude": ["src/generated/**"],
  "languages": ["javascript", "python"],
  "failOn": ["orphaned-addition", "newly-orphaned", "superseded", "invalid-suppression"],
  "checks": ["npm run typecheck", "npm test"]
}
  • entries adds roots the graph cannot see, such as files a job runner loads by name.
  • extendExclude adds to the default excludes (node_modules , build output, virtualenvs, …).
  • languages limits analysis to some ofjavascript ,python , andjava .
  • failOn picks the finding kinds that failcheck .
  • checks are the commandsfix --verify runs.

All options, dormant rules, and suppressions are in docs/configuration.md.

// loose-ends: dormant v1 webhooks still arrive until the March cutover
export function handleV1Webhook() {}
def handle_v1_webhook(): ...

Java uses // like TypeScript. A loose-ends-file: comment applies to the whole file instead of the declaration below it.

Dormant markers and suppressions need a reason. Without one, they become findings themselves. See docs/configuration.md.

name: Loose Ends
on: pull_request
permissions:
  contents: read
  pull-requests: write # for the PR comment
jobs:
  liveness:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0 # the base commit must exist locally
      - uses: sid732/loose-ends@v1
  • GitHub Action (no server, free on public repos): adds inline annotations on the diff, keeps one PR comment up to date, writes the report to the job summary, and fails the job on blocking findings. It works for Python and Java repositories too, with no Node setup step.
  • GitHub App: the same report as check runs, for people who prefer to host a server and install once across many repositories.

Inputs (working-directory, fail-on, comment, sarif, …) and App setup are in docs/github.md.

  • MCP server:npx -y @loose-ends/mcp exposescheck_changes ,scan_project ,explain ,list_roots , andplan_cleanup . All of them are read-only.
  • Skills:skills/ has two shortSKILL.md files that tell an agent when to run the check and how to respond to each finding.
{ "mcpServers": { "loose-ends": { "command": "npx", "args": ["-y", "@loose-ends/mcp"] } } }
npx loose-ends fix --verify --patch cleanup.diff

fix turns blocking findings into file deletions and declaration removals and applies them to a scratch copy of the project. Then it runs your configured checks (npm test, npm run build, …) there and writes a patch. Anything that would leave a dangling import is skipped, with the reason listed. --apply writes the change to your tree, and only after verification passes.

npx loose-ends check --coverage coverage/coverage-final.json

Runtime hit counts from istanbul or lcov reports are attached to findings. Coverage never overrides the graph. It explains it: code the graph considers unreachable that still ran points to a the graph cannot see.

  • TypeScript and JavaScript: Node.js, React, Next.js, Vite, Express, NestJS, React Router framework mode, Vue, Svelte and SvelteKit, Astro, Nuxt, VitePress, npm/yarn/pnpm workspaces, browser extensions, and common test runners.
  • Python: packages and src layouts, relative and namespace imports,__init__.py re-exports and__all__ , scripts, notebooks, pyproject/setup.py entry points, Django, Flask, FastAPI, Celery, Alembic, Airflow, Streamlit, pytest, and dependencies inrequirements*.txt ,pyproject.toml ,setup.py ,setup.cfg , andPipfile .
  • Java: top-level classes, interfaces, enums, and records; imports, same-package and fully qualified references;main methods; Spring, Jakarta EE, CDI, JAX-RS, Quarkus, Micronaut, MyBatis, MapStruct, Dagger/Hilt, and Android; class names in resource files; Maven and Gradle (including version catalogs) dependencies; and Java code used from Kotlin.

A repository can mix all of them. How it works is described in docs/architecture.md.

  • Tracking stops at top-level declarations. An unused method inside a used class, or an unused property on a used object, is not reported.
  • Code reached only through dependency injection by name, string-based plugin registries, or reflection is invisible to the graph. Add it to entries or mark it withloose-ends: entry .
  • Java classes with framework annotations (@Service ,@Controller ,@Entity , …) count as entry points, so they are never reported as unused.
  • Kotlin and Groovy are only scanned for the Java names they use. Their own code is not analyzed.
  • Python getattr with a computed name and modules found throughsys.path changes are not followed.
  • Other languages are not analyzed.

Setup, pull request rules, and the release process are in CONTRIBUTING.md. To report a vulnerability, see SECURITY.md.

── more in #ai-tools 4 stories · sorted by recency
── more on @loose ends 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/loose-ends-a-check-f…] indexed:0 read:7min 2026-11-13 · —