Skip to content

About

Claude Code skill for anonymous web research via Tor — every search and fetch uses a fresh circuit, so target sites can't link your requests to each other or back to you.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

anon-research

Anonymous internet research, one Tor circuit per request.

A Claude Code skill for searching and fetching the web through Tor, with per-request stream isolation so target sites can't link two of your requests as coming from the same source — let alone back to you.

Demo terminal session: status check, search, blocked fetch, and successful Tor fetch

Why this exists

The web tools that ship with Claude Code are great, but none of them are private:

Tool Anonymous? JS rendering Notes
WebSearch (built-in) ❌ ✅ Goes through Anthropic
firecrawl ❌ ✅ Tied to your Firecrawl API key
enterprise-search n/a n/a Internal docs only
anon-research ✅ ❌ This skill — Tor + circuit isolation

If you're researching something where the act of asking is sensitive — competitor intel, a medical question, a legal topic, a security investigation — none of the above hide you from the destination site. This skill does.

How it works

Every search and every fetch is shelled out to curl with a unique random SOCKS5 auth pair against the local Tor daemon. Tor's default IsolateSOCKSAuth policy treats different auth as different streams, which means a brand-new circuit (different guard, middle, and exit relays) for each request.

sequenceDiagram
    autonumber
    participant U as You
    participant A as anon.py
    participant T as Tor SOCKS5<br/>127.0.0.1:9050
    participant N as Tor network
    participant S as Target site

    U->>A: search "X"
    A->>A: random auth #1<br/>(secrets.token_hex(8))
    A->>T: CONNECT, user=abc123:x
    T->>N: build NEW circuit A<br/>guard → middle → exit A
    N->>S: HTTPS from Exit IP A
    S-->>U: response

    Note over A,T: every call gets a fresh auth → fresh circuit

    U->>A: fetch "Y"
    A->>A: random auth #2
    A->>T: CONNECT, user=def456:x
    T->>N: build DIFFERENT circuit B<br/>guard → middle' → exit B
    N->>S: HTTPS from Exit IP B
    S-->>U: response

    Note over S: site sees two unrelated requests<br/>from two different Tor users
Loading

The trick in one line:

curl --proxy socks5h://$(openssl rand -hex 8):x@127.0.0.1:9050 ...
#                       └──────────────────┘
#                       random per call → new circuit

The socks5h (with h) sends DNS through Tor too, so your ISP's resolver never sees the hostnames you visit either.

Quick start

# One-time setup — installs Tor (Homebrew on macOS; prints package-manager
# instructions on Linux) and starts the daemon. Idempotent.
~/.claude/skills/anon-research/scripts/anon.py setup

# Prove it works — two requests should report different exit IPs:
~/.claude/skills/anon-research/scripts/anon.py status

# Use it:
~/.claude/skills/anon-research/scripts/anon.py search "your query"
~/.claude/skills/anon-research/scripts/anon.py fetch  https://example.org/article

Or just talk to Claude — phrases like "search anonymously for X", "private research on Y", or "fetch this page without being tracked" trigger the skill via its description keywords.

Commands

setup

Detects platform, installs Tor if missing (brew install tor on macOS; prints apt/dnf/pacman hint on Linux; otherwise links to https://www.torproject.org/download/), starts the daemon, verifies connectivity. Idempotent — safe to re-run.

enable-control-port (opt-in, advanced)

Adds ControlPort 9051 and CookieAuthentication 1 to your Tor config and restarts Tor. Required once before --exit-country works. Idempotent — running twice is a no-op. See Exit country (advanced).

status

Makes two requests to check.torproject.org/api/ip with different SOCKS auth and prints both IPs. Both must say IsTor: true and the IPs must differ — that's the proof your two requests aren't correlatable by exit IP.

search QUERY [--limit N] [--json] [--engine ddg|searx]

Search the web through Tor. Default engine is DuckDuckGo: tries DDG's .onion service first (search traffic stays inside the Tor network — no exit relay sees your query) and falls back to the clearnet HTML mirror over Tor if the onion is unreachable. Both routes are full-Tor; the fallback isn't a privacy regression. Returns markdown list of {title, url, snippet} — or JSON with --json.

Pass --engine searx to use a SearXNG instance instead — see Search engines below.

Search engines

--engine ddg (default) — DuckDuckGo. Onion-first with clearnet-over-Tor fallback. Works out of the box.

--engine searx — SearXNG. Bring your own instance: point ANON_SEARX_URL at it.

export ANON_SEARX_URL=https://my-searx.example.com
~/.claude/skills/anon-research/scripts/anon.py search "..." --engine searx

Why BYO: every public SearXNG instance we tested in 2026 (searx.be, baresearch.org, search.inetol.net, …) returns 403 or 429 to Tor exits. SearXNG is heavily abused for scraping so most operators rate-limit Tor traffic aggressively. If you want this fallback to work, you'll need to self-host (it's a small Docker container — see SearXNG docs) or use one you have a relationship with.

The skill talks to the instance over Tor using the JSON API (/search?q=…&format=json). Make sure your instance has format: [html, json] enabled in its settings.yml.

Exit country (advanced)

--exit-country CC constrains Tor's exit relay to a specific country, useful for geo-restricted research (UK news paywalls, EU-only studies, regional pricing pages, etc.). Pass a 2-letter ISO code: us, gb, de, ca, nl, jp, …

anon.py search "regional news" --exit-country gb
anon.py fetch  https://example.com/region-locked --exit-country de

This requires Tor's control port. Enable it once:

~/.claude/skills/anon-research/scripts/anon.py enable-control-port

That subcommand:

  • Locates your torrc (/opt/homebrew/etc/tor/torrc on Apple-silicon brew, /usr/local/etc/tor/torrc on Intel brew, /etc/tor/torrc on Linux),
  • Appends ControlPort 9051, CookieAuthentication 1, CookieAuthFile <path>, CookieAuthFileGroupReadable 1,
  • Restarts Tor and verifies cookie-auth round-trips.

It's idempotent: re-running it is a no-op once the block is in place. If you've already configured a ControlPort yourself, the subcommand refuses to touch your torrc — open it and add CookieAuthentication 1 by hand.

Implementation: per call, anon.py opens a control connection, issues SETCONF ExitNodes={CC}, SETCONF StrictNodes 1, SIGNAL NEWNYM, runs the request, and RESETCONFs both keys on exit. Single-process serial use only — running two anon.py invocations concurrently with different --exit-country values will race on the global ExitNodes setting, since it's set per-Tor-instance.

fetch URL [--format markdown|text|html] [--exit-country CC]

GETs URL through Tor with a fresh circuit. Default output is readable markdown. Pass --exit-country to constrain the exit relay to a specific country (see below).

If the fetch hits a Tor block (Cloudflare challenge, HTTP 403/429/451, common captcha bodies), it prints BLOCKED: with the reason and exits 2 — never silently falls back to clearnet. Silent fallback would deanonymize you.

Privacy properties

Property How
Hide your IP from target sites All traffic via Tor SOCKS5
Hide DNS lookups from your ISP socks5h:// (DNS resolves inside Tor)
Two requests can't be linked by exit IP Random SOCKS auth per call → fresh circuit (IsolateSOCKSAuth)
No cookies linking requests across calls curl invoked without -b/-c; no session jar
No Referer leakage Header never sent
No Tor Browser fingerprint mimicry Generic Firefox-on-Linux UA — a common, low-signal choice
HTTPS-only HTTP URLs print a warning before sending
No silent clearnet fallback Block detection + BLOCKED: + exit 2

Limitations

  • No JavaScript — curl-based; SPAs and JS-heavy sites don't render. For those, use firecrawl and accept the privacy tradeoff.
  • Many sites block Tor exits — Cloudflare, Google, news paywalls. Returns BLOCKED. Try a different source, or re-run for a fresh exit.
  • Don't authenticate — Logging into anything from this skill destroys the anonymity property. Just don't.
  • Slower than direct HTTP — Building a fresh Tor circuit adds 5–15 seconds per request. That's the privacy/speed tradeoff this skill makes deliberately.
  • Your ISP can see you're using Tor — Just not what you're doing. Bridges/pluggable transports are out of scope for v1.

Repo layout

anon-research/
├── README.md           ← this file
├── SKILL.md            ← Claude Code frontmatter + invocation docs
├── assets/
│   └── demo.svg        ← terminal demo embedded above
└── scripts/
    └── anon.py         ← the CLI (stdlib only, shells out to curl)

Zero pip-installable dependencies. Just Python 3, curl, and a running Tor.

Verification log

Real output captured during install + verification
$ scripts/anon.py setup
==> Checking for tor binary…
    not found. Platform: Darwin
==> Installing Tor via Homebrew (this can take a minute)…
==> Pouring tor--0.4.9.6.arm64_tahoe.bottle.tar.gz
🍺  /opt/homebrew/Cellar/tor/0.4.9.6: 26 files, 31.2MB
==> Successfully started `tor` (label: homebrew.mxcl.tor)
==> Verifying Tor connectivity…
    OK. Tor ready on 127.0.0.1:9050 (exit IP: 37.228.129.241)

$ scripts/anon.py status
Two requests with different SOCKS auth (→ different circuits):
  request 1: IP=37.114.50.27   IsTor=True
  request 2: IP=192.42.116.52  IsTor=True
OK: Tor working with per-request circuit isolation.

$ scripts/anon.py fetch https://check.torproject.org/ | head -2
# Congratulations. This browser is configured to use Tor.
You are now free to browse the Internet anonymously.

$ scripts/anon.py fetch 'https://www.google.com/search?q=test'; echo "exit: $?"
BLOCKED: https://www.google.com/search?q=test
  reason: HTTP 429
  HTTP status: 429
  This skill is fail-closed — never falls back to clearnet.
exit: 2

In one full session we observed four distinct Tor exit IPs across just five requests: 37.228.129.241, 37.114.50.27, 192.42.116.52, 185.220.101.54. Stream isolation is doing its job.

Installing on a fresh machine

mkdir -p ~/.claude/skills
git clone <this-repo> ~/.claude/skills/anon-research
chmod +x ~/.claude/skills/anon-research/scripts/anon.py
~/.claude/skills/anon-research/scripts/anon.py setup

Claude Code auto-discovers anything under ~/.claude/skills/. After install, the skill activates automatically when you ask Claude to research something privately.

Threat model — what this doesn't protect against

Be honest about scope:

  • Active deanonymization by a global passive adversary correlating Tor entry and exit traffic. Mitigation: out of scope.
  • Browser fingerprinting (canvas, fonts, WebGL). N/A — no browser involved here.
  • You typing your real name into a search query. This is on you.
  • Logged-in services. Don't do it.
  • Tor exit nodes reading plaintext. That's why HTTPS is enforced.

This skill is a tool for unlinkable, IP-anonymous research from the perspective of target sites and your ISP — nothing more, nothing less.

About

Claude Code skill for anonymous web research via Tor — every search and fetch uses a fresh circuit, so target sites can't link your requests to each other or back to you.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages