# Find Dead Code in a Flask App

Yes. An agent runs vulture and your linters on a Flask app, checks each candidate against templates, plugin hooks and CLI commands, and writes a report.

[Run this on Rush](https://cloud.getrush.ai/run/find-dead-code/flask/), in a Project that has your repository.

## What the agent does

Dead-code tools lie on a Flask app unless someone checks what the framework calls by name, so the agent starts by learning how the app registers things. In a forum like flaskbb/flaskbb, `flaskbb/app.py` builds the application in `create_app` and a chain of `configure_*` functions: `configure_blueprints`, `configure_extensions`, `configure_template_filters`, which pushes filters and globals such as `run_hook`, `get_management_navigation` and `is_htmx_request` into `app.jinja_env`, `configure_context_processors`, `configure_errorhandlers` and `load_plugins`. A function that no Python module imports can still be live because a Jinja template under `flaskbb/templates/`, 68 HTML files, calls it, or because `flaskbb/cli/` registers it as a Click command, or because a pluggy hook implementation in `flaskbb/settings/registry.py` or `flaskbb/plugins/models.py` is found by name.

The agent runs `vulture` over `flaskbb/` with a whitelist it builds from those registration points: every view bound through `register_view` in `flaskbb/utils/helpers.py`, every template filter and global, every `hookimpl`, every Click command, and SQLAlchemy columns and relationships, which vulture otherwise reports as unused attributes. It runs `ruff check` too, since `pyproject.toml` selects `F` (unused imports and variables) and `B`, and runs `mypy` with the project's configuration, which sets `warn_unused_ignores`. What survives the whitelist is the candidate list, and each candidate gets read in context before it is reported.

Two sources of dead code are specific to this codebase and get their own sections. `flaskbb/deprecation.py` defines `RemovedInFlaskBB4` and a `deprecated` decorator, and `flaskbb/app.py` still warns about `CELERY_*` settings, a deprecated `CACHE_TYPE` and `RATELIMIT_STORAGE_URL`, so the report lists every deprecated path and whether anything inside the app still calls it. The plugin system in `flaskbb/plugins/spec.py` declares 78 `flaskbb_*` hook specifications across 1,579 lines; the agent greps `hook.<name>` calls in Python and `run_hook("<name>")` calls in templates and lists the specifications nothing calls, which is where template hooks such as `flaskbb_tpl_profile_links` and `flaskbb_tpl_form_registration_before` land if no template invokes them. A hook that nothing calls is dead for every plugin that implements it.

For each candidate it reports as safe, the agent proves it on a scratch branch: delete the function, run `uv run pytest` with the `-n auto` distribution `pyproject.toml` sets, run `ruff check` and `mypy`, and keep the candidate only if all three pass; the 76 test files under `tests/unit` and `tests/endtoend` are the oracle. It writes `dead-code.md`, pushes nothing and opens no pull request: deleting is a decision for the maintainers, made with the file and line of each item in front of them.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Find dead code in this Flask application and write dead-code.md. Read flaskbb/app.py, flaskbb/utils/helpers.py, flaskbb/plugins/spec.py, flaskbb/cli/main.py, flaskbb/deprecation.py and pyproject.toml first and list how views, template filters, Jinja globals, Click commands and pluggy hooks are registered by name. Run vulture over flaskbb/ with a whitelist built from those registration points and from SQLAlchemy columns, then ruff check and mypy with the project's configuration. Read every remaining candidate in context. For each one you call safe, delete it on a scratch branch and run uv run pytest, ruff check and mypy; keep it in the safe list only if all three pass. Add a section for deprecated code paths under flaskbb/deprecation.py and the warnings in flaskbb/app.py, and a section for hook specifications in flaskbb/plugins/spec.py that no Python code or template calls. Give file, line and signature for every item and the commands that proved it. Do not push and do not 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 `dead-code.md` as an artifact on the session. It has four parts: functions, methods and classes safe to delete, grouped by package with file, line, signature and the three commands that passed without them; deprecated paths with the version they are slated for and whether the app still calls them; hook specifications nothing calls, each with the `spec.py` line; and the candidates vulture flagged that turned out to be live, with the template, command or hook that keeps them alive, so the next person does not re-investigate them.

The transcript at `GET /o/{org}/sessions/{id}/messages` keeps the vulture output, the whitelist the agent built and every scratch-branch test run, so a maintainer can see the exact `pytest` summary behind a "safe" verdict. The repository is not changed; the session lists no pull request.

Before deleting anything, a maintainer reads the safe list against the plugin API: a public helper unused inside FlaskBB may still be imported by `flaskbb-plugin-portal` or `flaskbb-plugin-conversations`, which `pyproject.toml` installs, and the report flags items that are importable from a plugin for that reason.

## Run it on every first week of a release cycle

Create an automation in the Project with this prompt and a `schedule` trigger, with a `cron` such as `0 9 1 * *` in your `timezone`, so the report is fresh at the start of each cycle. Each fire starts a run on the current default branch, and the report sits on that run's session; compare the safe lists from two runs to see what the cycle removed.

From a terminal, the same run starts with this body in a `POST /o/{org}/sessions`, and the result arrives on the session or through the key's `run.completed` webhook; the file is at `GET /o/{org}/sessions/{id}/artifacts/dead-code.md`.

## Why run it on Rush?

Proving a function dead in FlaskBB means a `uv sync` with the test, typing and pre-commit groups, then a `pytest` run per deletion, which the `-n auto` setting spreads over every core. On Rush each run gets its own sandbox and its own clone, the scratch branches stay there, and the sandbox parks when the turn ends, so a follow-up asking to recheck one item against a plugin starts from the same state.

The report is only as good as its proof, and the proof is in the transcript: the vulture output, the whitelist, each deletion and each test run. A maintainer who disagrees with one verdict can see what was tried, and several repositories, or several branches of this one, can be audited at the same time.

## Frequently asked questions

### We already run ruff with F401 and F841. What does this add?

Ruff finds unused imports and local variables inside a file. Vulture finds functions, methods and classes that nothing in the whole package references, and the agent then separates the real ones from the ones Flask, Jinja, Click or pluggy call by name. The scratch-branch test run is the part neither tool does.

### Will it delete anything from my repository?

No. Deletions happen on a scratch branch inside the sandbox to prove a verdict and are never pushed. The output is the report; a follow-up run can turn the safe list into a pull request if you ask for one.

### How does it handle the plugin hooks?

It reads `flaskbb/plugins/spec.py`, collects every `hook.<name>` call in Python and every `run_hook` call in a template, and lists the specifications with no caller. It also marks public helpers that a plugin could import, because the plugin packages in `pyproject.toml` are consumers the core's own tests do not cover.

### Can a function be reachable and still appear in the report?

Yes, when the only caller is a string: a template, a Click command name, a Celery task name or a setting fixture. The report puts those in the "flagged but live" section with the string that keeps each one alive, which is why every candidate is read before it is listed as safe.
