🛡️ No Phone Home
Find the telemetry that doesn't ask permission.
A privacy-focused static analyzer that traces your machine's data to the network — and flags every path that never checks whether you said no.
Install · Quick start · CI setup · Custom rules · How it works
The problem
Traditional scanners — Semgrep, Bandit, CodeQL — hunt CVEs and OWASP classes. Silent telemetry isn't a vulnerability in that sense, so it walks straight through every existing gate.
No Phone Home asks the question a privacy auditor actually asks:
Does this code take data about my machine and my identity, ship it over the network, and never check whether I said no?
It answers by running source-to-sink taint analysis over tree-sitter ASTs, then verifying whether each network egress is genuinely gated by a consent check such as DO_NOT_TRACK.
🔍 What it catches
1. Unguarded exfiltration
import socket, requests
host = socket.gethostname() # 1. tainted source
payload = {"device": host} # 2. taint propagates
requests.post("https://telemetry.dev/api", json=payload) # 3. egress, no guard ❌
2. Inverted guards — the case most tools get backwards
if os.getenv("DO_NOT_TRACK"):
requests.post(url, json=payload) # ⛔ transmits *because* you opted out
A naive "is there an enclosing if mentioning DO_NOT_TRACK?" check calls this safe. No Phone Home reasons about polarity — which way the consent token points, and whether it is truthy or falsy where the sink actually runs — so it reports this at HIGH severity as a distinct INVERTED_GUARD verdict.
The same logic works across languages. In Go:
host, _ := os.Hostname()
if os.Getenv("DO_NOT_TRACK") != "" {
http.Post(url, "application/json", host) // ⛔ sends only when DNT *is* set
}
3. Guard clauses — how real code is actually written
def report():
if os.getenv("DO_NOT_TRACK"):
return # ✅ dominating guard, correctly cleared
requests.post(url, json={"host": socket.gethostname()})
An ancestor-only if walk misses this entirely, because a guard clause is not an enclosing conditional.
4. Telemetry hidden one call deep
def _send(payload):
requests.post(URL, json=payload) # sink lives here
def track():
_send({"user": getpass.getuser()}) # ❌ still caught, via function summaries
This is how telemetry SDKs are actually structured, and it is invisible to a purely intraprocedural pass.
📦 Installation
Requires Python 3.10+. No other runtime — the tree-sitter grammars ship as prebuilt wheels.
From GitHub (works today):
pip install git+https://github.com/mkamranr/no-phone-home.git
From source (for development):
git clone https://github.com/mkamranr/no-phone-home.git
cd no-phone-home
pip install -e ".[dev]"
pytest # 67 tests
Using uv:
uv venv && uv pip install -e ".[dev]"
uv run no-phone-home ./src
Note · A PyPI release (
pip install no-phone-home) is planned — see the release workflow. Until then, install from GitHub as above.
🚀 Quick start
no-phone-home ./src # scan a directory
no-phone-home app.py # scan one file
no-phone-home . --format markdown # Privacy Nutrition Label
no-phone-home . --list-rules # see active rules
Typical output:
┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Sev ┃ Rule ┃ Location ┃ Private data → destination ┃ Guard ┃
┡━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ HIGH │ ST-001 │ metrics.py:34 │ socket.gethostname() → │ UNGUARDED │
│ │ │ │ https://telemetry.dev/api │ │
│ HIGH │ ST-002 │ analytics.py:12 │ getpass.getuser() → │ INVERTED │
│ │ │ │ https://api.mixpanel.com │ │
└─────────┴────────┴───────────────────┴──────────────────────────────┴────────────┘
23 files scanned · 8 taint paths · 5 privacy violations
⛔ 1 inverted guard(s): data is sent on the branch where the user opted OUT.
📋 Output formats
| Format | Flag | Use it for |
|---|---|---|
| Table | --format table (default) |
Reading in a terminal |
| Privacy Nutrition Label | --format markdown |
PR comments, READMEs, audit reports |
| SARIF 2.1.0 | --format sarif |
GitHub code scanning — taint traces render as clickable code flows |
| JSON | --format json |
Scripting and dashboards |
The Privacy Nutrition Label gives non-engineers a readable summary:
## 📋 Privacy Nutrition Label
| Data Category | Detected Source | Egress Destination | Guard Status |
| :--- | :--- | :--- | :--- |
| **System Info** | `platform.node()` | `https://telemetry.dev/api` | ❌ **UNGUARDED** |
| **User Identity** | `os.userInfo()` | `https://api.mixpanel.com` | ✅ Guarded (`DO_NOT_TRACK`) |
| **Environment** | `process.env` | Local Log File | ℹ️ Internal Only |
⚙️ Continuous integration
GitHub Actions
No Phone Home ships as a composite action. SARIF uploads land in your repo's Security tab:
name: Privacy scan
on: [push, pull_request]
permissions:
contents: read
security-events: write
jobs:
no-phone-home:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mkamranr/no-phone-home@v0.1.0
with:
path: ./src
fail-on: HIGH
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: no-phone-home.sarif
Or call the CLI directly:
- run: pip install git+https://github.com/mkamranr/no-phone-home.git
- run: no-phone-home ./src --format sarif --output results.sarif --fail-on HIGH
Exit codes
| Code | Meaning |
|---|---|
0 |
Clean — no violations at or above the threshold |
1 |
Violations found at or above --fail-on |
2 |
Execution error (bad path, unreadable rules file) |
Distinguishing 1 from 2 is what makes it safe to gate a build on.
pre-commit
repos:
- repo: https://github.com/mkamranr/no-phone-home
rev: v0.1.0
hooks:
- id: no-phone-home
🎛️ CLI reference
no-phone-home [PATH] [OPTIONS]
| Option | Default | Description |
|---|---|---|
PATH |
. |
File or directory to scan |
-f, --format |
table |
table, json, markdown, sarif |
-o, --output |
stdout | Write the report to a file |
--fail-on |
HIGH |
Exit 1 at/above this severity, or never |
--min-confidence |
LOW |
Drop findings below HIGH / MEDIUM / LOW |
--include-deps |
off | Also scan node_modules/, .venv/, vendor/ |
--rules |
auto | Path to a .nph-rules.yml |
--baseline |
none | Ignore findings recorded in this file |
--write-baseline |
— | Snapshot current findings, then exit 0 |
--no-gitignore |
off | Don't honour .gitignore |
--exclude |
— | Extra directory name to skip (repeatable) |
--list-rules |
— | Print active rules and exit |
--version |
— | Print version and exit |
Full details in docs/usage.md.
📐 Custom rules
Rules are declarative YAML — extending coverage needs no Python. Drop a .nph-rules.yml in your repo root:
version: "1.0"
rules:
- id: ST-001
name: Unchecked Hostname Exfiltration
severity: HIGH
language: python
category: SYSTEM_INFO
sources:
- pattern: "socket.gethostname()"
- pattern: "platform.node()"
sinks:
- pattern: "requests.post($URL, data=$DATA)"
guards:
- pattern: "os.getenv('DO_NOT_TRACK')"
remediation: "Wrap the request in an explicit opt-out check."
Patterns are parsed with the same tree-sitter grammar as your code, so they're real syntax rather than regexes. $NAME is a metavariable wildcard.
🔇 Suppression
Inline, scoped to a specific rule:
requests.post(url, json=payload) # no-phone-home: ignore[ST-001]
Or snapshot an existing codebase and fail only on new findings:
no-phone-home . --write-baseline .nph-baseline.json # once
no-phone-home . --baseline .nph-baseline.json # in CI
🌐 Language support
All three languages run the same engine — taint propagation, same-file function summaries, and polarity-aware guard verification. Only rule coverage differs.
| Language | Extensions | Engine | Builtin rules |
|---|---|---|---|
| Python | .py .pyi |
✅ Full | 6 |
| JavaScript / TypeScript | .js .mjs .cjs .jsx .ts .tsx .mts .cts |
✅ Full | 4 |
| Go | .go |
✅ Full | 4 |
Adding a language means implementing one adapter protocol — the analyzer never names a tree-sitter node type directly. See docs/architecture.md.
🧠 How it works
[ Source Code ]
│
▼
[ Parser (tree-sitter) ] ──► Language ASTs (Python, JS/TS, Go)
│
▼
[ Normalized IR ]
│
▼
[ Taint Engine ]
├── Source matching (system / user / hardware APIs)
├── Propagation (assignment, dicts, f-strings, concat)
├── Function summaries (same-file, cross-function)
└── Sink matching (HTTP clients / telemetry SDKs)
│
▼
[ Guard Evaluator ] ──► GUARDED / UNGUARDED / INVERTED_GUARD / LOCAL_ONLY
│
▼
[ Reporters ] ──► Table · Nutrition Label · JSON · SARIF
⚠️ Honest limitations
This is a heuristic static analyzer, not a proof of absence.
It reads code without running it, so it will miss dynamically-constructed exfiltration (getattr, eval, reflection, runtime-assembled URLs), and it will sometimes flag benign code. A clean scan is not a certificate of privacy.
Its value is making silent telemetry visible and reviewable — not certifying innocence. Every finding carries a confidence level precisely so you can tell "definitely" from "worth a look".
Scope boundaries:
- First-party code only by default. Vendor directories are skipped;
--include-depsopts in, at the cost of slower scans and findings you can't directly fix. - Intraprocedural + same-file summaries. Taint crossing module boundaries is not tracked; a whole-program call graph is where scanners lose accuracy and speed.
- Hashing a value lowers confidence but does not clear taint — a hashed hardware ID is still a stable identifier.
📚 Documentation
| Document | Contents |
|---|---|
| docs/usage.md | Complete CLI reference, workflows, CI recipes, troubleshooting |
| docs/rules.md | Writing custom rules, pattern syntax, guard vocabulary |
| docs/architecture.md | How the engine works, adding a language |
| CONTRIBUTING.md | Development setup, fixture requirements |
| SECURITY.md | Reporting vulnerabilities, threat model |
| CHANGELOG.md | Release history |
🤝 Contributing
The highest-value contribution is rules and fixtures — both are YAML and plain sample code, no engine knowledge needed.
Every detection change ships with fixtures in both directions: a true_positive/ case and the correctly-guarded true_negative/ form. The corpus harness enforces 100% precision and recall, which is deliberate — for a privacy scanner a false positive costs more than a miss, because it's what gets the tool deleted from CI.
See CONTRIBUTING.md.
📄 License
Metadata
Release files for no-phone-home 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| no_phone_home-0.1.0.tar.gz | 74.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| no_phone_home-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 127.0 kB
Release files / no_phone_home-0.1.0.tar.gz
| Download URL | no_phone_home-0.1.0.tar.gz |
|---|---|
| Size | 74.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a6c7d6f7657f56e81d070e58b32235c4b24ccb3e7cb1cd7f20d1b6e9d94c2e2b
|
|
BLAKE2b-256 checksum How to use checksums |
913ea7c879dd09c785ab9f0a2733c4af7da15de2cf5bc32f07dda375e1f5fafd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / no_phone_home-0.1.0-py3-none-any.whl
| Download URL | no_phone_home-0.1.0-py3-none-any.whl |
|---|---|
| Size | 52.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a96253646672b1e60db3a7f7b8ee6078463c2396477b70bc605ed9c4735c8067
|
|
BLAKE2b-256 checksum How to use checksums |
ff05e9e37dbcc3cf330fe1322c26102d48d28991d1cd53ac9c5b9e99907d1a18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log