Skip to main content

🛡️ 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.

CI PyPI Downloads Python 3.10+ License: Apache 2.0 SARIF 2.1.0 Languages

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, so Go and TypeScript code is scanned without Go or Node installed.

pip install no-phone-home

Or with pipx / uv, to keep it out of your project's dependencies:

pipx install no-phone-home
uv tool install no-phone-home

Both no-phone-home and the short alias nph are installed:

no-phone-home ./src
nph ./src              # identical

From source (for development):

git clone https://github.com/mkamranr/no-phone-home.git
cd no-phone-home
pip install -e ".[dev]"
pytest          # 68 tests

🚀 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
nph . --fail-on HIGH                     # short alias, same tool

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, available on the GitHub Marketplace. 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
        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 no-phone-home
- 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.1
    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.

📖 Full rule-authoring guide →


🔇 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

📖 Architecture deep dive →


⚠️ 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-deps opts 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

Apache-2.0

Built for developers and auditors who'd rather know what their dependencies are sending home.

Metadata

Release files for no-phone-home 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for no-phone-home 0.1.1
File Size Uploaded
no_phone_home-0.1.1.tar.gz 76.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for no-phone-home 0.1.1
File Interpreter ABI Platform
no_phone_home-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 128.9 kB

Release files / no_phone_home-0.1.1.tar.gz

Download URL no_phone_home-0.1.1.tar.gz
Size 76.3 kB
Tags Source
SHA-256 checksum
How to use checksums
dfc3d1724b8c5535735b3026f2cec83bae13b5d5ffda40a1fc01190dc049b271
BLAKE2b-256 checksum
How to use checksums
5ad1d86ce439b8a198f6698461a2fc53495cca184620adef641da5efafe5b1ca
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

Release files / no_phone_home-0.1.1-py3-none-any.whl

Download URL no_phone_home-0.1.1-py3-none-any.whl
Size 52.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7940e01bab1b4968cdcbedb84774802a90ace14c009609bd91fdf1c5eb779ea2
BLAKE2b-256 checksum
How to use checksums
218e60ce38dc0025374d0fd24135037629274d252eb090ea620f295e823d4f7a
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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page