Dead code is a security risk when it is still deployed or still installed: an old route nobody patches, a dependency with a known CVE that nothing imports, a credential left in a test fixture. Code that is truly unreachable is a smaller risk, but it still adds noise to every security scan and review.

Most teams treat dead code as a "clean up someday" problem. The security problem is that unused code is unowned code, and unowned code doesn't get patched.

TL;DR

  • Dead code increases your attack surface without providing any value
  • Unused dependencies can keep known CVEs in your scan results even when the application no longer needs the package
  • Dead endpoints and routes create hidden entry points for attackers
  • Removing dead code makes vulnerability triage and security review smaller
  • Modern tools can detect dead-code candidates, but deletion still needs tests, ownership checks, and review

The problem: your codebase is full of ghosts

Here's what typically happens:

You refactor authentication. The old code path gets commented out "just in case". Six months later, someone finds it, assumes it's still used, and patches a critical bug everywhere except the dead code.

Then an attacker discovers the old endpoint. They exploit the unpatched vulnerability. Your logs show no warnings because nobody knew that code still existed.

This pattern is easy to fall into: once nobody owns the old path, nobody patches it.

Why dead code is a security problem

1. Unmaintained code doesn't get security patches

# Old authentication endpoint (supposedly removed in v2.0)
@app.route('/api/v1/login')
def legacy_login():
    username = request.args.get('username')
    password = request.args.get('password')
    # SQL injection vulnerability - never patched because "it's not used anymore"
    query = f"SELECT * FROM users WHERE name='{username}' AND pass='{password}'"
    result = db.execute(query)
    return jsonify(result)

# New secure endpoint
@app.route('/api/v2/login')
def login():
    username = request.form.get('username')
    password = request.form.get('password')
    # Properly parameterized query
    result = db.execute("SELECT * FROM users WHERE name=? AND pass=?", (username, password))
    return jsonify(result)

Your security team patched the SQL injection in the new endpoint. The old one? Still vulnerable. Still reachable. Still exploitable.

2. Unused dependencies are a supply chain risk

# requirements.txt
flask==2.3.0
django==4.2.0          # Not actually imported anywhere
requests==2.28.0
Pillow==9.5.0          # Used in a deleted feature, still installed
beautifulsoup4==4.11.0

That unused Django dependency? CVE-2023-43665 - Denial of Service vulnerability.

The Pillow library from the deleted image upload feature? CVE-2023-44271 - Denial of service (uncontrolled memory allocation).

You're paying the security cost of dependencies you don't even use.

3. Dead code hides your real attack surface

When your security team audits the codebase, they need to know what's actually running in production.

If a large share of your code is dead, your security review spends time on code that doesn't matter and can miss the code that does.

Attackers don't have this problem. They probe everything.

4. Commented-out code becomes stale fast

# def process_payment(amount, card_number):
#     # TODO: Add encryption
#     response = requests.post(
#         PAYMENT_API,
#         data={'card': card_number, 'amount': amount}  # Sends card in plaintext!
#     )
#     return response.json()

def process_payment(amount, payment_token):
    # New implementation uses tokenized payments
    response = requests.post(
        PAYMENT_API,
        data={'token': payment_token, 'amount': amount}
    )
    return response.json()

Commented code doesn't get refactored. It doesn't get updated when APIs change. And if someone uncomments it "temporarily" during an incident? You just reintroduced a plaintext credit card transmission vulnerability.

5. Old test fixtures expose real data

# tests/fixtures/users.py
TEST_USERS = [
    {
        'username': 'admin',
        'password': 'temp_admin_2023',  # This was the actual prod password once
        'api_key': 'sk_live_abc123...',  # Real API key that still works
        'email': 'john.smith@company.com'
    }
]

Test fixtures get copy-pasted from production. Then they sit in the repo forever, even after the test is deleted. Now your git history contains valid credentials.

The hidden costs

Dead code doesn't just create vulnerabilities. It also:

  • Slows down security scans - More code = longer scan times
  • Creates false positives - Scanners flag issues in code that never runs
  • Increases CI/CD time - You're testing code that doesn't ship
  • Confuses new developers - "Is this code path still used?"
  • Makes audits harder - reviewers have to account for code that nobody owns

Real-world impact

The practical impact is not a universal percentage. It depends on how much legacy code, unused dependency surface, and stale route surface a repo carries.

What is consistent is the security workflow problem: dead code makes scanners inspect code that no longer matters, makes reviewers reason about paths nobody owns, and makes dependency findings harder to prioritize. The more code you keep around "just in case," the harder it becomes to know what is actually in production.

What makes code "dead"

Not all unused code is obvious:

Obviously dead

  • Functions never called
  • Imports never used
  • Commented-out blocks
  • Unreachable code after return or raise

Subtly dead

  • Feature flags that are permanently disabled
  • A/B test variants that lost and were "turned off"
  • Fallback code paths that are no longer possible
  • Backend endpoints for deleted frontend features
  • Database migration rollback functions (after 2 years in prod)

Dead by isolation

  • Modules that nothing imports
  • Classes instantiated only in other dead code
  • API endpoints that no clients call anymore

How to fix it

Step 1: Detect dead code automatically

Manual code review doesn't scale. You need automated detection.

Look for tools that can:

  • Trace imports - Which modules are actually loaded?
  • Analyze call graphs - Which functions are reachable from entry points?
  • Check framework routes - Which endpoints are registered?
  • Scan dependencies - Which packages are actually imported?
  • Framework-aware detection - Understands Flask, Django, FastAPI patterns
# Dead code only
skylos .
# Add unused declared dependencies (SKY-U005), unreachable code and security checks
skylos . --quality --danger

Step 2: Prioritize by risk

Not all dead code is equally dangerous. Start with:

  1. Dead endpoints - Direct attack surface
  2. Unused dependencies - Supply chain risk
  3. Commented security code - Easy to accidentally re-enable
  4. Old authentication/auth code - High-value targets
  5. Everything else

Skylos treats registered Flask, FastAPI and Django routes as entry points, so it will not report a route like legacy_login above as dead. Find unused endpoints from access logs or gateway metrics instead. skylos . --danger will still flag the SQL injection inside it (SKY-D211).

Step 3: Remove it safely

Don't just delete everything at once. Use a process:

# Before deletion: Add deprecation warning
@app.route('/api/v1/old_endpoint')
def old_endpoint():
    logger.warning("DEPRECATED: old_endpoint called - delete this route if the log stays silent for 2-4 weeks")
    # ... old code

Monitor for 2-4 weeks. If nothing breaks, delete it.

Step 4: Keep it clean

Automate dead code checks in CI:

# .github/workflows/dead-code-check.yml
name: Dead code check
on:
  pull_request:
permissions:
  contents: read
jobs:
  dead-code:
    name: Skylos dead code
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Setup Python
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - name: Install Skylos
        run: python -I -m pip install "skylos==5.1.1"
      - name: Check for dead code
        run: skylos . --diff-base "origin/$GITHUB_BASE_REF" --diff "origin/$GITHUB_BASE_REF" --strict

--strict makes every reported finding block the PR while retaining repository context for liveness analysis. Ordinary line-attached dead-code findings are diff-filtered, but repository-wide circular-dependency findings can still come from unchanged files; review the first run before requiring the job.

For a generated workflow with annotations and review comments, run skylos cicd init --no-upload.

Tools that help

For Python:

  • Skylos - Dead code + security analysis with framework awareness
  • Vulture - Finds unused code (no built-in framework model; configure --ignore-decorators or whitelists)
  • Coverage.py - Shows what code runs in tests
  • Pylint - Unused imports, variables and arguments, and unused private members (W0611, W0612, W0613, W0238)

For other languages:

  • JavaScript/TypeScript: Knip
  • Java: IntelliJ inspections
  • Go: deadcode (golang.org/x/tools/cmd/deadcode), staticcheck, go mod tidy

Start with the riskiest dead code

You don't need to achieve 0% dead code. Start with the slice that carries the most risk:

  • Old authentication code
  • Unused dependencies with known CVEs
  • Dead endpoints and routes
  • Commented-out security-sensitive code

Get these out of production first.

Common objections

"We might need it later." Git history keeps it. Reverting a removal commit brings it back.

"It's not hurting anything." Reachable dead routes and installed-but-unused dependencies are attack surface; the rest is review and scan noise.

"What if it breaks something?" It can: static tools mistake code called by name for dead code. Remove in small PRs, run your tests, and log before deleting routes (below).

Start small

Pick one area to clean up this week:

  1. Run a dead code scanner on your main service
  2. Remove unused imports from your 5 most-changed files
  3. Delete one commented-out function that's been dead for >6 months
  4. Uninstall one unused dependency

Track the results:

  • How many lines removed?
  • Did CI get faster?
  • Did security scan results get cleaner?
  • Any production incidents?

The target outcome is simple: a smaller review surface, cleaner scan output, and no production incidents from removing code that still had a hidden caller.

The bottom line

Dead code is a code-quality problem and, where it is still reachable or installed, a security problem. Unused code in production:

  • can add attack surface (routes, dependencies)
  • creates maintenance burden
  • blurs what is really running
  • costs security review time

The solution isn't perfect hygiene. It's consistent, automated cleanup that removes the highest-risk dead code first, so dependency and security findings are easier to triage.


Want to find dead code in your Python projects? pip install skylos, then skylos . -a reports dead code, unused declared dependencies, security issues and vulnerable dependencies in one run. Install the CLI.