An architecture where an AI agent processes documents without sending files to an outside service is appealing. PaperPilot, a PDF tool written in Rust, says its CLI, MCP server, browser WASM build, desktop app, and localhost:7823 REST API all share one engine.

But "runs locally" and "the local API is safe" are different sentences. A commenter on the Hacker News thread said the project's local API allows requests from every website, and I checked that claim against the current source. First, the limits: I did not install or run PaperPilot, and I read the source on GitHub's web view.

Confirmed project facts

Per the README. This is not independent verification.

Item Detail
Repository KrushnaVardhanReddy/PaperPilot (original). hobostay/PaperPilot is a fork of it
License Apache 2.0 for the Community Edition; enterprise features are separate
Surfaces MCP server, CLI, browser WASM, Tauri desktop, localhost:7823 REST API
Features (v0.6) merge/split/rotate, compression, structured text extraction, OCR (rten), watermarking, PII scan, Excel conversion, and more
Privacy claims files never leave the machine, "no cloud calls", the browser version processes in memory
Scale 674 stars, 1,648 commits on main (per the repo page)

The repo page did not confirm a release date or version. A secondary source mentions v0.6.1 on October 26, which I could not verify directly. That matches the briefing's "first public date unconfirmed."

One detail stands out. The README's web embed example loads a script from cdn.usepaperpilot.com, which is a network request. "No cloud calls" is therefore best read as describing the local tools, not the embed. Either way, checking the browser Network tab, as the README itself suggests, is the surest test.

Checking the HN claim against the source

The commenter (devy) wrote that paperpilot-gateway/src/server.rs:95-98 allows every origin with no authentication. That is one user's assertion, not something I verified, and the same comment also claims about 98% of commits are by AI bots. I checked only one spot in the server code. This is the main-branch server.rs I fetched:

// Restrict CORS: Allow only trusted local origins (Tauri desktop client, Vite dev server, localhost)
let cors = CorsLayer::new()
    .allow_origin(tower_http::cors::AllowOrigin::predicate(|origin, _| {
        let s = origin.to_str().unwrap_or("");
        s.starts_with("tauri://")
            || s.starts_with("http://localhost")
            || s.starts_with("http://127.0.0.1")
            || s.starts_with("https://localhost")
            || s.starts_with("https://127.0.0.1")
    }))
    .allow_methods(Any)
    .allow_headers(Any);

What I found:

  • The "allows every origin" claim does not match the version I saw. The code appears intended to pass only specific origins. It may have been fixed after the comment, but I could not check the commit history.
  • The prefix (starts_with) check does have a flaw: http://localhost.evil.example passes.
  • No authentication middleware was visible in this file. It could live elsewhere, so I do not say there is none.
  • The bind address is not fixed in this file; the caller decides. I could not confirm whether it is 127.0.0.1 or 0.0.0.0.

Why prefix matching is a problem

I copied that decision logic and ran it myself. It is a replica of the logic, not the real PaperPilot binary.

from urllib.parse import urlparse
def prefix_predicate(s):
    return (s.startswith("tauri://") or s.startswith("http://localhost") or s.startswith("http://127.0.0.1")
            or s.startswith("https://localhost") or s.startswith("https://127.0.0.1"))
def exact_predicate(s):
    u = urlparse(s)
    return u.scheme == "tauri" or (u.scheme in ("http", "https") and u.hostname in ("localhost", "127.0.0.1"))
http://localhost:5173              prefix=True  exact-host=True
tauri://localhost                  prefix=True  exact-host=True
https://evil.example               prefix=False exact-host=False
http://localhost.evil.example      prefix=True  exact-host=False
http://127.0.0.1.evil.example      prefix=True  exact-host=False

If an attacker owns a domain like localhost.evil.example and the user opens that page, the origin the browser sends passes the prefix check. Scripts on that page could then call localhost:7823 on the user's machine, and without authentication that might reach tools that touch local files. That last link, whether the API processes arbitrary file paths (the HN comment's mcp-exec point), I did not verify, so I do not use it as a risk assessment. Parsing the host and comparing it exactly is the usual fix.

Checklist before connecting a local tool to an agent

Not specific to PaperPilot; these apply to any local server or MCP tool.

  1. Confirm the local REST server binds only to 127.0.0.1 (for example with lsof -i :7823).
  2. Watch the browser dev tools Network tab for outside requests during processing. Try a sample PDF first.
  3. Assume a web page on the same computer can call an unauthenticated local HTTP API, and read whether the CORS allow-list compares hosts exactly.
  4. Check whether an MCP tool accepts arbitrary file paths or restricts allowed directories.
  5. Try sensitive documents only after testing with fake data.

Takeaway

One engine behind many surfaces is an interesting design. But "everything is local" is a design intent, and the moment a local port opens, a new security boundary exists. My checking covers a single source file, and it is not an assessment of the project's overall safety or quality. An unverified HN claim is just a claim, and my counter-evidence holds only within what I checked.

Related reading

Sources