Skip to content

Security: KeilerHirsch-Labs/schroedinger-sync

Security

SECURITY.md

Security Policy & Threat Model

This tool decrypts a Windows DPAPI-protected cookie store and uses the extracted sessionKey to authenticate as you against claude.ai. That is, byte-for-byte, the same technique a credential-stealing trojan uses. We're not going to pretend otherwise, and a license disclaimer alone would not make it safe. What actually makes it safe is the architecture below — enforced in code (security_test.go), not just promised in this document.

What this tool cannot do

1. It cannot target another user's account. DPAPI master keys are bound to the Windows user account that created them. There is no code path anywhere in this project that can decrypt another user's cookie store — it only ever works within the OS session it's run in, against whatever account is logged into Claude Desktop on that machine. See loadMasterKey() in main.go.

2. It cannot run covertly. Claude.ai sits behind a Cloudflare managed JS challenge that headless browsers cannot pass. This tool must launch a real, visible Chrome window to clear it (chromedp.Flag("headless", false) — hardcoded, never a flag or env var; enforced by TestHeadlessIsHardcoded). An attacker who had already compromised a victim's session well enough to run this tool covertly would see a browser window pop up on the victim's screen — a terrible trade against any purpose-built infostealer, which doesn't need a GUI at all. This friction is the point, not an oversight.

On the DPAPI/EDR question, since it comes up: CryptUnprotectData against a browser's own cookie store is the exact behavioral signature (MITRE ATT&CK T1555.003) that credential-stealing malware uses, and endpoint security products flag tools built on it — Microsoft Defender classifies GhostPack's SharpDPAPI/SharpChrome, LaZagne, and NirSoft's WebBrowserPassView this way even when signed, because that detection is behavioral, not signature-based. Code signing alone does not make this go away; that's expected, not a bug in this project's plan. Two things are worth keeping distinct: (1) what this tool does today — CryptUnprotectData against Claude Desktop's own cookie store, decrypting exactly one row, only ever within the OS session that created it (see point 1) — is the offensive-shaped side of that primitive, and any serious reviewer should expect an EDR to notice; (2) the planned encryption-at-rest work (README roadmap) uses CryptProtectData/a TPM-backed CNG key to wrap a key this program generates itself, which is the ordinary, defensive use of the same API family — the same mechanism Chrome, Windows, and ASP.NET Core Data Protection all use to store their own secrets. Conflating the two is an easy mistake for a scanner or a skimming reviewer to make; this document exists so a human doesn't have to.

3. It cannot exfiltrate anything. Every output goes to local disk (desktop-chats/) or local MemPalace. There is no network client anywhere in this codebase capable of sending data to a third-party server — no webhook, no telemetry, no "phone home." Every outbound network call targets claude.ai and nothing else, enforced by TestNetworkEgressIsClaudeOnly, which scans the source for every literal request destination and fails the build if one doesn't resolve to claude.ai. (That test's actual boundary: it only catches literal string URLs — a request destination built via a variable or string concatenation wouldn't be caught. The stronger, structural guarantee is that there is no net/http import anywhere in this module — see go.mod — so there is no general-purpose HTTP client to redirect in the first place.)

4. The sessionKey is never observable in this program's own output. The moment it's decrypted, it's registered with a redactor (security.go) that scrubs it from stdout (installStdoutRedactor — replaces os.Stdout with a pipe so every write, from this code or any future third-party library targeting stdout, is scrubbed before reaching the terminal), from chromedp's own internal logging (chromedp.WithErrorf/WithLogf, wired in cdp.go's openClaudeSession — without this, chromedp's library defaults to Go's stdlib log package, which targets stderr and would otherwise bypass the stdout redactor entirely while network.Enable() is processing the injected sessionKey cookie for the whole session), and the daemon's log file and probe-report.txt (both route through the same redact() call). Enforced by TestRedactionScrubsRegisteredSecret and TestStdoutRedactorSurvivesSplitWrites. (Scope: this covers every output path this program controls. It does not protect against out-of-band forensics of the process's own memory — a debugger or memory dump taken by someone who already has code-execution as the same Windows user, who could just call CryptUnprotectData on Local State/Cookies directly instead. Since v2.2.0, the raw AES master key and raw decrypted plaintext ([]byte, unlike the immutable string this function returns) are explicitly zeroed (security.go's zeroBytes, deferred in readSessionKey) the moment their last read completes — this narrows, but does not close, the window an attacker with that level of access would have; the returned sessionKey string itself is still held for the session's lifetime by design, both as the CDP cookie value in use and in the redactor's own registry, so it can keep scrubbing output for as long as the program runs.)

5. Only the minimum secret is ever decrypted. Earlier versions of this tool decrypted the entire claude.ai cookie jar (session key, Cloudflare tokens, everything). readSessionKey() now decrypts exactly one row — the sessionKey cookie — because CDP only ever needs that one value; Chrome earns its own cf_clearance by solving the challenge itself. Smaller blast radius: exactly one decrypted (plaintext) secret is ever held in process memory. (Note: copyCookieDB() does copy the entire Cookies SQLite file — every domain, still DPAPI/AES-GCM-encrypted at rest — to a temp directory for every session, with best-effort cleanup that isn't guaranteed if the process is killed mid-run. That's a broader on-disk footprint than "one secret" might suggest by itself, though nothing in that copy is ever decrypted except the single sessionKey row.)

6. It's not published as a reusable library. Everything lives in package main, not an importable module (TestNoImportableCookiePackage). Someone wanting to reuse the DPAPI-decrypt or Cloudflare-bypass primitives for an unrelated purpose has to read and copy the code deliberately — not go get it as a drop-in ingredient.

7. Rate-limited by design. getWithRetry applies exponential backoff on rate_limit_error responses. This exists because heavy sessions legitimately hit transient rate limits, but it also means this tool cannot be trivially turned into a hammer against Anthropic's infrastructure.

What "enforced by tests" actually means

TestHeadlessIsHardcoded and TestNetworkEgressIsClaudeOnly are regex/substring scans of the source, not a general-purpose static analyzer. They're regression tripwires — built to catch an accidental reintroduction of headless mode or a stray non-claude.ai network call during ordinary maintenance — not a hard security boundary that would survive a deliberately adversarial change. Concretely: splitting an override across two lines specifically to dodge TestHeadlessIsHardcoded's same-line proximity check, or building a request URL via a variable instead of a literal (invisible to TestNetworkEgressIsClaudeOnly), would both slip through undetected. For a solo-maintained project where the realistic risk is "I changed something and didn't notice the consequence," not "a malicious contributor with commit access," that's an honest and appropriate scope for what these two tests buy you. TestNoImportableCookiePackage is the one test in this set that's robust against deliberate evasion — dodging it requires literally restructuring the module out of package main.

What this license does not do

We deliberately did not add an "ethical use only" clause to the LICENSE. Such clauses are legally close to unenforceable and mostly function as security theater — they make the author feel better without stopping anyone. The actual protection is everything above: architectural constraints, enforced by tests, that make this tool a poor choice for anything other than exporting your own data from your own account on your own machine.

Business model: free for you, commercial licence for closed-source embedding

This project's original 2026-03 plan was a freemium SaaS (free tier, paid Pro/Team tiers aimed at individuals). That plan is retired. The individual tool is, and will remain, free and open under the AGPLv3 — no paid tier for the person running it against their own account, no telemetry, no upsell.

What is not free is one specific thing: the right to take this code and ship it inside a closed-source or SaaS product without honouring the AGPLv3. That is a separate commercial licence (see COMMERCIAL-LICENSE.md), aimed at organisations, not at the individual end user. The reasoning:

  • The real addressable audience is a technical niche (people running Claude Desktop
    • VS Code Claude Code + a local memory system like MemPalace), not a mass consumer market. There is no mass-market paid tier to chase — and none is chased.
  • A tool that decrypts your own credentials should never ask you to pay a stranger for the privilege. For the individual, free + open source is the whole value exchange: read exactly what it does before trusting it with DPAPI access. The commercial licence is not charged to that person — it is charged to a company that wants to build a closed product on top and escape the copyleft.
  • The AGPLv3 is the mechanism, not an afterthought. Its network-copyleft is what makes the free edition genuinely free (any hosted derivative must also open its source) and what makes a commercial licence meaningful (a company that won't open its source buys the exception instead). Free software ships "AS IS, WITHOUT WARRANTY" (see LICENSE); any warranty or support obligations would be negotiated separately in a commercial licence, not carried by the free edition.

Scope: Claude/Anthropic only (for now)

This is intentionally scoped to claude.ai. Other providers (ChatGPT, Gemini, ...) each have their own auth model, cookie format, and challenge/anti-bot behavior — building correct, equally-hardened support for each is a separate effort, not a quick extension of this codebase. Not a limitation we plan to paper over; a deliberate v2 scope decision.

Reporting a vulnerability

Open a private security advisory on this repository, or contact @KeilerHirsch directly. Do not open a public issue for a security report.

Supported versions

Version Supported
v2.x (CDP-based) Yes
v1.x (tls-client impersonation, removed) No — superseded, code deleted

There aren't any published security advisories