A static analysis tool for Python codebases written in Python that detects unreachable functions and unused imports, aka dead code. Faster and better results than many alternatives like Flake8 and Pylint, and finding more dead code than Vulture in our tests with comparable speed.
The cli will output the results in a table format with the appropriate flags
- Features
- Benchmark
- Installation
- Quick Start
- Skylos Gate
- VS-Code extension
- Web Interface
- Design
- Multi-Language Support
- Test File Detection
- Vibe Coding
- AI Audit
- Quality
- Ignoring Pragmas
- Including & Excluding Files
- CLI Options
- Example Output
- Interactive Mode
- Development
- CI/CD (Pre-commit & GitHub Actions)
- FAQ
- Limitations
- Troubleshooting
- Contributing
- Roadmap
- License
- Contact
- CST-safe removals: Uses LibCST to remove selected imports or functions (handles multiline imports, aliases, decorators, async etc..)
- Framework-Aware Detection: Attempt at handling Flask, Django, FastAPI routes and decorators
- Test File Exclusion: Auto excludes test files (you can include it back if you want)
- Interactive Cleanup: Select specific items to remove from CLI
- Unused Functions & Methods: Finds functions and methods that not called
- Unused Classes: Detects classes that are not instantiated or inherited
- Unused Imports: Identifies imports that are not used
- Folder Management: Inclusion/exclusion of directories
- Ignore Pragmas: Skip lines tagged with
# pragma: no skylos,# pragma: no cover, or# noqa - NEW Secrets Scanning (PoC, opt-in): Detects API keys & secrets (GitHub, GitLab, Slack, Stripe, AWS, Google, SendGrid, Twilio, private key blocks)
- NEW Dangerous Patterns: Flags risky code such as
eval/exec,os.system,subprocess(shell=True),pickle.load/loads,yaml.loadwithout SafeLoader, hashlib.md5/sha1. Refer toDANGEROUS_CODE.mdfor the whole list. This includes SQL injection, path traversal and any other security flaws that may arise from the practise of vibe-coding.
The benchmark checks how well static analysis tools spot dead code in Python. Things such as unused functions, classes, imports, variables, that kinda stuff. To read more refer down below.
The methodology and process for benchmarking can be found in BENCHMARK.md
| Tool | Time (s) | Items | TP | FP | FN | Precision | Recall | F1 Score |
|---|---|---|---|---|---|---|---|---|
| Skylos (Local Dev) | 0.013 | 34 | 22 | 12 | 7 | 0.6471 | 0.7586 | 0.6984 |
| Vulture (0%) | 0.054 | 32 | 11 | 20 | 18 | 0.3548 | 0.3793 | 0.3667 |
| Vulture (60%) | 0.044 | 32 | 11 | 20 | 18 | 0.3548 | 0.3793 | 0.3667 |
| Flake8 | 0.371 | 16 | 5 | 7 | 24 | 0.4167 | 0.1724 | 0.2439 |
| Pylint | 0.705 | 11 | 0 | 8 | 29 | 0.0000 | 0.0000 | 0.0000 |
| Ruff | 0.140 | 16 | 5 | 7 | 24 | 0.4167 | 0.1724 | 0.2439 |
To run the benchmark:
python compare_tools.py /path/to/sample_repo
Note: More can be found in BENCHMARK.md
pip install skylosgit clone https://github.com/duriantaco/skylos.git
cd skylos
pip install .skylos /path/to/your/project
skylos /path/to/your/project --secrets ## include api key scan
skylos /path/to/your/project --danger ## include safety scan for dangerous code
skylos /path/to/your/project --quality ## include quality scan for complex code
skylos /path/to/your/project --secrets --danger --quality ## you can string all the flags together
skylos /path/to/your/project --danger --quality --audit --model claude-haiku-4-5-20251001 ## if u want to add a LLM for auditing
skylos /path/to/your/project --danger --quality --audit --fix --model claude-haiku-4-5-20251001 ## for automated fixing
# To launch the front end
skylos run
# Interactive mode - select items to remove
skylos --interactive /path/to/your/project
# Comment out items
skylos . --interactive --comment-out
# Dry run - see what would be removed
skylos --interactive --dry-run /path/to/your/project
# Load the results in json format
skylos --json /path/to/your/project
# Load the results in table format
skylos --table /path/to/your/project ## the current skylos versoin does not use table anymore, this is kept for backward compatability and will be deprecated in the next update
# Load the results in tree format
skylos --table /path/to/your/project
# With confidence
skylos path/to/your/file --confidence 20 ## or whatever value u wanna setSkylos also has a Quality Gate. It prevents bad code, security risks, and spaghetti logic from entering your repository.
Stop using default settings. Generate a pyproject.toml configuration file to define your team's standards.
skylos initThis creates a [tool.skylos] section in your pyproject.toml in which you can adjust the rules:
[tool.skylos]
# 1. Global Defaults (Applies to all languages)
complexity = 10
nesting = 3
max_args = 5
max_lines = 50
ignore = []
model = "gpt-4.1"
# 2. Language Overrides (Optional)
[tool.skylos.languages.typescript]
complexity = 15
nesting = 4
[tool.skylos.gate]
# Gatekeeper Policy
fail_on_critical = true
max_security = 0
max_quality = 10
strict = false
Use the --gate flag to enforce these rules. If the scan fails, Skylos exits with an error code (blocking CI/CD or git push).
skylos . --quality --danger --gateYou will then be asked a series of questions on whether you will like to push. You can choose to select the files manually or push all at once.
Skylos has a VS Code extension that runs on save like a linter. Runs automatically on save of Python files. You will see highlights + a popup like "Skylos found N items." For more information, refer to the VSC README.md inside the marketplace.
The extension runs on Skylos engine. By default, the quality, danger and secrets engine should be running. If it is not, you can turn it on inside settings.
From VS Code Marketplace: "Skylos" (publisher: oha)
Version: 0.2.0
Skylos includes a modern web dashboard for interactive analysis:
skylos run
# Opens browser at http://localhost:5090Framework endpoints are often invoked externally (eg, via HTTP, signals), so we use framework aware signals + confidence scores to try avoid false positives while still catching dead codes
Name resolution handles aliases and modules, but when things get ambiguous, we rather miss some dead code than accidentally mark live code as dead lmao
Tests are excluded by default because their call patterns are noisy. You can opt in when you really need to audit it
Skylos uses a confidence-based system to try to handle Python's dynamic nature and web frameworks.
- Confidence 100: 100% unused (default imports, obvious dead functions)
- Confidence 60: Default value - conservative detection
- Confidence 40: Framework helpers, functions in web app files
- Confidence 20: Framework routes, decorated functions
- Confidence 0: Show everything
When Skylos detects web framework imports (Flask, Django, FastAPI), it applies different confidence levels:
# only obvious dead codes
skylos app.py --confidence 60 # THIS IS THE DEFAULT
# include unused helpers in framework files
skylos app.py --confidence 30
# include potentially unused routes
skylos app.py --confidence 20
# everything.. shows how all potential dead code
skylos app.py --confidence 0Skylos uses a Router Architecture to support multiple languages. It automatically detects file extensions and routes them to the correct analyzer.
Skylos uses tree-sitter for robust TypeScript parsing.
- Dead Code: Finds unused functions, classes, interfaces, and methods.
- Security (--danger): Detects eval(), innerHTML XSS, and React
dangerouslySetInnerHTML. - Quality (--quality): Calculates Cyclomatic Complexity for TS functions.
Note: You do not need to install Node.js. The parser is built into Skylos.
Skylos automatically excludes test files from analysis because test code patterns often appear as "dead code" but are actually called by test frameworks. Should you need to include them in your test, just refer to the Folder Management
File paths containing:
/test/or/tests/directories- Files ending with
_test.py
Test imports:
pytest,unittest,nose,mock,responses
Test decorators:
@pytest.fixture,@pytest.mark.*@patch,@mock.*
When Skylos detects a test file, it by default, will apply a confidence penalty of 100, which will essentially filter out all dead code detection
# This will show 0 dead code because its treated as test file
/project/tests/test_user.py
/project/test/helper.py
/project/utils_test.py
# The files will be analyzed normally. Note that it doesn't end with _test.py
/project/user.py
/project/test_data.py We are aware that vibe coding has created a lot of vulnerabilities. To an AI, it's job is to spit out a list of tokens with the highest probability, whether it's right or not. This may introduce vulnerabilities in their applications. We have expanded Skylos to catch the most important problems first.
- SQL injection (cursor)
- SQL injection (raw-API)
- Command injection
- SSRF
- Path traversal
- eval/exec
- pickle.load/loads
- yaml.load w/o SafeLoader
- weak hashes MD5/SHA1
- subprocess(..., shell=True)
- requests(..., verify=False)
skylos /path/to/your/project --danger# SQLi (cursor)
cur.execute(f"SELECT * FROM users WHERE name='{name}'")
# SQLi (raw-api)
pd.read_sql("SELECT * FROM users WHERE name='" + q + "'", conn)
# Command injection
os.system("zip -r out.zip " + folder)
# SSRF
requests.get(request.args["url"], timeout=3)
# Path traversal
with open(request.args.get("p"), "r") as f: ...
This list will be expanded in the near future. For more information, refer to DANGEROUS_CODE.md
Skylos now integrates with LLMs to fix bugs and audit code logic. We only support OpenAI and Anthropic models for now. We will integrate more models and providers in the upcoming months
skylos . --audit Note: If you leave this empty, the default is gpt-4.1
If you want to select your own model
skylos . --audit --quality --model claude-haiku-4-5-20251001By default, Skylos will run a few checks namely:
- Vibe Coding Detection: Checks if code calls functions that do not actually exist in the repo.
- Secret Leaks: Scans comments and variable assignments for hardcoded secrets.
- Logic Flaws: Detects confusing logic or bare exceptions.
- Dangerous Codes
skylos . --fixSupports OpenAI and Anthropic.
Secure: API keys are asked for once and stored in your OS Keychain (Windows Credential Locker, macOS Keychain, etc.).
Static checks that highlight functions likely to be hard to maintain (even if they're not "dead code"). Off by default. You can enable with --quality.
Cyclomatic complexity (a.k.a. “McCabe”): counts your decision points (ifs, loops, try/except, comprehensions, boolean ops, etc.).
Nesting depth: how many levels deep your control-flow is. Deeply nested code is harder to read and refactor.
skylos /path/to/your/project --quality
- Quality Issues ================
-
[Complexity | Warn] app.omg_quality @ /path/app.py:1 High cyclomatic complexity: 13 (target ≤ 10), 27 lines. -> This function has a lot of branching and loops. -> Suggested fix: split parts into smaller helpers or simplify nested if/else logic.
-
[Nesting | Medium] app.omg_quality @ /path/app.py:1 Deep nesting: depth 3 (target ≤ 2), 27 lines. -> This function has a lot of branching and loops. -> Suggested fix: use guard clauses / flatten branches.
- Complexity 13 (target ≤ 10): the computed cyclomatic complexity is 13; the default target is 10.
- Nesting depth 3 (target ≤ 2): control-flow is 3 levels deep.
DO NOTE:
Right now thresholds are code-level constants (keeps the CLI simple). If you need to tweak:
-
Open
skylos/rules/quality/quality.py -
Change these functions:
- scan_complex_functions(ctx, threshold=10)
- scan_nesting(ctx, threshold=2)
Config file support is on the roadmap
- To ignore any warning, indicate
# pragma: no skylosON THE SAME LINE as the function/class you want to ignore
Example
def with_logging(self, enabled: bool = True) -> "WebPath": # pragma: no skylos
new_path = WebPath(self._url)
return new_path- To suppress a secret on a line, add:
# skylos: ignore[SKY-S101]
Example
API_KEY = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]By default, Skylos excludes common folders: __pycache__, .git, .pytest_cache, .mypy_cache, .tox, htmlcov, .coverage, build, dist, *.egg-info, venv, .venv
skylos --list-default-excludes
# Exclude single folder (The example here will be venv)
skylos /path/to/your/project --exclude-folder venv
# Exclude multiple folders
skylos /path/to/your/project --exclude-folder venv --exclude-folder build
# Force include normally excluded folders
skylos /path/to/your/project --include-folder venv
# Scan everything (no exclusions)
skylos path/to/your/project --no-default-excludes Usage: skylos [OPTIONS] PATH
Arguments:
PATH Path to the Python project to analyze
Options:
-h, --help Show this help message and exit
--json Output raw JSON instead of formatted text
--table Output results in table format via the CLI
-o, --output FILE Write output to file instead of stdout
-v, --verbose Enable verbose output
--version Checks version
-i, --interactive Interactively select items to remove
--dry-run Show what would be removed without modifying files
--exclude-folder FOLDER Exclude a folder from analysis (can be used multiple times)
--include-folder FOLDER Force include a folder that would otherwise be excluded
--no-default-excludes Don't exclude default folders (__pycache__, .git, venv, etc.)
--list-default-excludes List the default excluded folders and
-c, --confidence LEVEL Confidence threshold (0-100). Lower values will show more items.
-- secrets Scan for api keys/secrets
-- danger Scan for dangerous code
The interactive mode lets you select specific functions and imports to remove:
- Select items: Use arrow keys and
spacebarto select/unselect - Confirm changes: Review selected items before applying
- Auto-cleanup: Files are automatically updated
Pick one (or use both)
-
GitHub Actions: runs Skylos on pushes/PRs in CI.
- No local install needed
-
Pre-commit (local + CI): runs Skylos before commits/PRs.
- You must install pre-commit locally once. Skylos gets installed automatically by the hook.
- Create .github/workflows/skylos.yml (COPY THE ENTIRE SKYLOS.YAML FROM BELOW):
name: Skylos Deadcode Scan
on:
pull_request:
push:
branches: [ main, master ]
workflow_dispatch:
jobs:
scan:
runs-on: ubuntu-latest
env:
SKYLOS_STRICT: ${{ vars.SKYLOS_STRICT || 'false' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Skylos
run: pip install skylos
- name: Run Skylos
env:
REPORT: skylos_${{ github.run_number }}_${{ github.sha }}.json
run: |
echo "REPORT=$REPORT" >> "$GITHUB_OUTPUT"
skylos . --json > "$REPORT"
id: scan
- name: Fail if there are findings
continue-on-error: ${{ env.SKYLOS_STRICT != 'true' }}
env:
REPORT: ${{ steps.scan.outputs.REPORT }}
run: |
python - << 'PY'
import json, sys, os
report = os.environ["REPORT"]
data = json.load(open(report, "r", encoding="utf-8"))
count = 0
for value in data.values():
if isinstance(value, list):
count += len(value)
print(f"Findings: {count}")
if count > 0:
print(f"::warning title=Skylos findings::{count} potential issues found. See {report}")
sys.exit(1 if count > 0 else 0)
PY
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ steps.scan.outputs.REPORT }}
path: ${{ steps.scan.outputs.REPORT }}
- name: Summarize in job log
if: always()
run: |
echo "Skylos report: ${{ steps.scan.outputs.REPORT }}" >> $GITHUB_STEP_SUMMARYTo make the job fail on findings (strict mode):
-
Go to GitHub -> Settings -> Secrets and variables -> Actions -> Variables
-
Add variable SKYLOS_STRICT with value true
. Create or edit .pre-commit-config.yaml at the repo root:
A: Skylos hook repo
## .pre-commit-config.yaml
repos:
- repo: https://github.com/duriantaco/skylos
rev: v2.6.0
hooks:
- id: skylos-scan
name: skylos report
entry: python -m skylos.cli
language: python
types_or: [python]
pass_filenames: false
require_serial: true
args: [".", "--output", "report.json", "--confidence", "70", "--danger"]
- repo: local
hooks:
- id: skylos-fail-on-findings
name: skylos
env:
SKYLOS_SOFT: "1"
language: python
language_version: python3
pass_filenames: false
require_serial: true
entry: >
python -c "import os, json, sys, pathlib;
p=pathlib.Path('report.json');
if not p.exists():
sys.exit(0);
data=json.loads(p.read_text(encoding='utf-8'));
count = 0
for v in data.values():
if isinstance(v, list):
count += len(v)
print(f'[skylos] findings: {count}');
sys.exit(0 if os.getenv('SKYLOS_SOFT') or count==0 else 1)"B: self-contained local hook
repos:
- repo: local
hooks:
- id: skylos-scan
name: skylos report
language: python
entry: python -m skylos.cli
pass_filenames: false
require_serial: true
additional_dependencies: [skylos==2.6.0]
args: [".", "--output", "report.json", "--confidence", "70"]
- id: skylos-fail-on-findings
name: skylos (soft)
language: python
language_version: python3
pass_filenames: false
require_serial: true
entry: >
python -c "import os, json, sys, pathlib;
p=pathlib.Path('report.json');
if not p.exists():
sys.exit(0);
data=json.loads(p.read_text(encoding='utf-8'));
count = 0
for v in data.values():
if isinstance(v, list):
count += len(v)
print(f'[skylos] findings: {count}');
sys.exit(0 if os.getenv('SKYLOS_SOFT') or count==0 else 1)"Install requirements:
You must install pre-commit locally once:
pip install pre-commit
pre-commit install-
pre-commit run --all-files
-
Run the same hooks in CI (GitHub Actions): create .github/workflows/pre-commit.yml:
name: pre-commit
on: [push, pull_request]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11", cache: "pip" }
- uses: pre-commit/action@v3.0.1
with: { extra_args: --all-files }Pre commit behavior: the second hook is soft by default (SKYLOS_SOFT=1). This means that it prints findings and passes. You can remove the env/logic if you want pre-commit to block commits on finding
Python ≥3.9pytestinquirer
git clone https://github.com/duriantaco/skylos.git
cd skylos
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activatepython -m pytest tests/Q: Why doesn't Skylos find 100% of dead code? A: Python's dynamic features (getattr, globals, etc.) can't be perfectly analyzed statically. No tool can achieve 100% accuracy. If they say they can, they're lying.
Q: Why are the results different on my codebase? A: These benchmarks use specific test cases. Your code patterns (frameworks, legacy code, etc.) will give different results.
Q: Are these benchmarks realistic? A: They test common scenarios but can't cover every edge case. Use them as a guide, not gospel.
Q: Should I automatically delete everything flagged as unused? A: No. Always review results manually, especially for framework code, APIs, and test utilities.
Q: Why did Ruff underperform? A: Like all other tools, Ruff is focused on detecting specific, surface-level issues. Tools like Vulture and Skylos are built SPECIFICALLY for dead code detection. It is NOT a specialized dead code detector. If your goal is dead code, then ruff is the wrong tool. It is a good tool but it's like using a wrench to hammer a nail. Good tool, wrong purpose.
Q: Why doesn't Skylos detect my unused Flask routes?
A: Web framework routes are given low confidence (20) because they might be called by external HTTP requests. Use --confidence 20 to see them. We acknowledge there are current limitations to this approach so use it sparingly.
Q: What confidence level should I use? A: Start with 60 (default) for safe cleanup. Use 30 for framework applications. Use 20 for more comprehensive auditing.
Q: What does --danger check?
A: It flags common security problems. Refer to DANGEROUS_CODE.md for the full details
- Dynamic code:
getattr(),globals(), runtime imports are hard to detect - Frameworks: Django models, Flask, FastAPI routes may appear unused but aren't
- Test data: Limited scenarios, your mileage may vary
- False positives: Always manually review before deleting code
- Secrets PoC: May emit both a provider hit and a generic high-entropy hit for the same token. All tokens are detected only in py files (
.py,.pyi,.pyw) - Quality limitations: The current
--qualityflag does not allow you to configure the cyclomatic complexity.
-
Permission Errors
Error: Permission denied when removing functionCheck file permissions before running in interactive mode.
-
Missing Dependencies
Interactive mode requires 'inquirer' packageInstall with:
pip install skylos[interactive]
We welcome contributions! Please read our Contributing Guidelines before submitting pull requests.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Expand our test cases
- Configuration file support
- Git hooks integration
- CI/CD integration examples
- Deployment Gatekeeper
- Further optimization
- Add new rules
- Expanding on the
dangerous.pylist - Porting to uv
- Small integration with typescript
- Expand and improve on capabilities of Skylos in various other languages
- Expand the providers for LLMs
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.
- Author: oha
- Email: aaronoh2015@gmail.com
- GitHub: @duriantaco




