All jobs

Find Dead Code in a Go Project

Yes. An agent runs whole-program reachability analysis on your Go module, removes each unreachable function on a scratch branch to prove it, and writes a report.

Run this on RushIn a Project that has your repository.

What the agent does

The agent clones the repository into its own cloud sandbox and reads how the module is laid out before it runs any tool, because what counts as dead depends on what counts as an entry point. In a Go project like miniflux/v2 the module is miniflux.app/v2 on go 1.26.0, main.go does nothing but call cli.Parse(), and the code lives in 25 directories under internal/: api, cli, storage, reader, ui, template, worker and the rest, 332 non-test Go files with 91 test files beside them. One directory sits outside internal/: client/, the package miniflux.app/v2/client that other programs import. Nothing in the server calls it, so a naive analysis would flag its whole API as dead, and the agent marks it as public API before it starts.

It reads what the project already checks so the report adds something. .golangci.yml is a v2 config with the standard linters, which includes unused, plus staticcheck, gocritic, perfsprint and others, with errcheck disabled; Makefile has lint running go vet ./..., a gofmt check and golangci-lint run, and test running go test -cover -race -count=1 ./...; linters.yml runs the same lint in CI. unused already catches unexported identifiers nothing in their package references. What it cannot see is an exported function, method or type that every package has stopped calling, and that is the gap the report fills.

For that it runs deadcode from golang.org/x/tools/cmd/deadcode, which builds the whole program from main and reports functions no call path reaches, with -test so test functions count as roots and helpers only tests use are not listed. It reads every hit against the code rather than trusting it: a function reached only through an interface method set, or referenced as a value in the template.FuncMap that internal/template builds, is reachable and the tool knows it; a handler registered by string in a table is the kind of case worth a second look. It then checks each remaining candidate the only way that settles it: on a scratch branch it deletes the function, runs go build ./..., go vet ./... and go test ./..., and keeps the candidate only when all three still pass.

The report groups what survived by package, each entry with its file and line, its signature, and the commands that proved its removal safe, and a separate section for exported identifiers in client/ that the server never calls, labelled public API and excluded from the delete list. Candidates that failed the check are listed with the build or test error, so nobody repeats the experiment. The agent pushes nothing and opens no pull request; the report is the result, and a follow-up message can turn its first section into one.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Find dead code in this Go module. Read go.mod, main.go, Makefile and .golangci.yml first and note what the configured linters already catch. Run golang.org/x/tools/cmd/deadcode with -test from the main package, treating any package outside internal/ as public API that other programs import. For each reported function, read the code, then on a scratch branch delete it and run go build ./..., go vet ./... and go test ./...; keep it only if all three pass. Write dead-code.md with the functions safe to delete grouped by package, each with file, line, signature and the commands that proved it; a separate section for exported public API nothing in the server calls; and every candidate that failed the check with its error. Do not push or open a pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is one file, dead-code.md, as an artifact on the session; reading the session returns a download link for it that works for an hour, and a fresh link on every read. Its first section lists the functions, methods and types that are safe to delete, grouped by package under internal/, each with its file and line, its signature, and the go build, go vet and go test output from the branch where it was removed. The second section lists exported identifiers in client/ that no server package calls, labelled as public API and not recommended for deletion. The third lists each candidate deadcode named that did not survive removal, with the error.

A reviewer reads the first section against their own knowledge of the codebase: a function kept deliberately for a planned feature, or called by a tool outside the repository, gets struck off. What remains can be deleted in one pull request, and the report already names the files. Nothing in the repository changed; the scratch branch lived only in the sandbox.

The session's transcript holds every command, including the full deadcode output before filtering, so the reviewer can see what was reported and why each line was kept or dropped.

Run it on every first morning of the quarter

Add an automation to the Project with a schedule trigger, a cron for the first morning of January, April, July and October in your timezone, and this prompt. Each run reads the repository as it is that morning, writes a fresh report and leaves it on the session, so dead code is a quarterly list to work through instead of an archaeology project.

A webhook on the API key covers runs your own server starts: Rush POSTs run.completed when the turn finishes, with a session_url from which a small handler reads the artifact link and posts it where the team will see it.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

Why run it on Rush?

The tool is the easy part; the proof is the slow part. Deleting a function and rebuilding and retesting a module with hundreds of files, once per candidate, is the kind of work nobody does by hand, so dead code stays until someone trips over it. The agent does that loop in its own sandbox, on a scratch branch that never leaves it, and your checkout is never touched.

A report that says a function is dead because a tool said so gets ignored. One that shows the build, vet and test output from the commit where that function was gone gets acted on, and the session keeps that output beside the report. Several modules can be audited at once, each in its own sandbox.

Frequently asked questions

We already run golangci-lint with unused. What does this add?

unused works one package at a time and reports unexported identifiers nothing in their package references. It cannot see an exported function that every other package has stopped calling, and in a module like this one, where almost everything lives under internal/ and is exported for its siblings, that is where the dead code accumulates. deadcode analyses the whole program from main, so it finds those.

Will it delete anything from my repository?

No. The deletions happen on a scratch branch in the sandbox to prove each candidate, and that branch is never pushed. The result is the report; a follow-up message to the same session can ask for a pull request from its first section, and it continues in the same sandbox with the work already done.

What about the client package other programs import?

Anything outside internal/ is treated as public API: the report lists its unreferenced exports in their own section so you can see them, and recommends none for deletion. The Go toolchain enforces that internal/ packages cannot be imported from outside the module, which is what makes their exports fair game.

Can a function be reachable and still show up?

Reflection and go:linkname are the known blind spots, and a function reached only through reflect would be reported. That is why no candidate goes in the first section on the tool's word alone: each one has to survive a build and the full test suite with it removed, and a package whose tests do not exercise a reflective path is called out so you know the check was weaker there.