Skip to content
TwinScope0.3.10

Privacy

TwinScope never writes file contents to disk and makes exactly one network call — an update check that is off until you turn it on. This page says precisely what it stores, where, and how to check that we are telling the truth.

Getting startedEdit this page

What is stored#

WhatWhereContents
Comparison historytwinscope.db in the user-data directory — on macOS, ~/Library/Application Support/TwinScope/One row per comparison: a title, the engine id, each side's kind, display name, path and size, the engine options, the summary counts, a starred flag and two timestamps
Preferencessettings.json, same directoryTheme, per-engine option defaults, an update-check flag, and the window's size and position
Exported reportsOnly the path you pick in the save dialogThe comparison you chose to export — which does include content, because that is what a report is
Pasted imagesA temporary directory, as a PNGA clipboard image has to become a file before the image engine can read it, so a paste writes one

History is capped: past 500 rows the oldest unstarred rows are dropped. Re-running the same pair updates its existing row instead of stacking duplicates, so the list stays a list of things you compared rather than of button presses. Clear history on the History screen deletes every row, starred ones included.

What is never stored#

File contents. Not in history, not in preferences, not in a cache, not in a log.

The history module strips contents on the way in rather than trusting whichever part of the app called it — a future caller cannot leak them by forgetting to:

src/main/history.tsts
/**
* The only shape that reaches the database. Contents are dropped here — not by
* the caller — so no future caller can leak them by forgetting.
*/
function strip(input: InputPayload): StoredInput {
return {
  kind: input.kind,
  name: input.name,
  ...(input.path !== undefined ? { path: input.path } : {}),
  size: input.size,
};
}

The Settings screen shows the two things you cannot turn on, as facts rather than as switches that pretend to do something: Telemetry (there is none) and Store file contents in history (off, permanently).

Click to enlarge
Fixed, not defaulted. A disabled switch is more honest than one that could be flipped.

Why the window cannot read your disk#

TwinScope treats its own renderer as untrusted, which is the standard threat model for anything that renders content: a bug in the UI layer should not become access to your filesystem.

MeasureEffect
sandbox: trueThe renderer process runs inside the OS sandbox
contextIsolation: truePage scripts and preload scripts cannot see each other's globals
nodeIntegration: falserequire, process and fs do not exist in the page
A narrow preload bridgeThe page can reach exactly nine keys on window.twinscope and nothing else — no ipcRenderer, and no route back to it
zod validation in mainEvery payload the renderer sends is parsed and validated at the IPC boundary before anything acts on it
Content-Security-PolicyA packaged build allows default-src 'self' and connect-src 'self'; scripts must come from the app itself
Navigation denialsNew windows, navigation away from the app's own page, and <webview> embedding are all refused
Permission denialsEvery permission request — camera, microphone, geolocation, notifications — is denied outright

The renderer's own build enforces the same boundary earlier: its TypeScript project omits Node's type definitions and a lint rule rejects any renderer file that imports node:*, so a violation fails the build rather than shipping. Files are opened by the two processes outside the sandbox: main, which reads a small input once so the drop zone can preview it, and the engine worker, which opens anything too large to carry. The window itself never opens a file.

Exports are the one thing that leaves#

An HTML or Markdown report contains the comparison, including content. That is the point of a report — but it only ever goes where you send it:

  • The format menu opens a native save dialog. Cancelling writes nothing.
  • The HTML report is self-contained: no scripts, no external stylesheet, no font or image fetched at open time. Images inside it are embedded as data: URLs, so the file works on someone else's machine and still calls nothing.
  • The unified patch is copied to your clipboard rather than written anywhere.

How to verify it yourself#

Three checks, none of which require trusting this page.

1. Read the database. It is plain SQLite, so look at exactly what is in it:

Terminalbash
cd ~/Library/Application\ Support/TwinScope
sqlite3 twinscope.db '.schema comparisons'
sqlite3 twinscope.db 'select input_a, input_b, summary from comparisons limit 5;'

You will see kinds, names, paths, sizes and counts. There is no column that could hold a file's contents, which is a stronger guarantee than a promise not to fill one.

2. Watch the network. With the app running and a comparison in flight, ask the OS what sockets it has open:

Terminalbash
lsof -i -nP | grep -i twinscope

It prints nothing. A firewall tool such as Little Snitch is the stricter version of the same check, and will never see a connection attempt to block.

3. Read the hardening. src/main/security.ts is about ninety lines and holds the whole security posture: the CSP, the navigation and <webview> blocks, and the permission handler. The window flags sit next to it in src/main/window.ts. The repository's boot test asserts all of it — that process, require and ipcRenderer are undefined in the page, that window.open is refused, and that navigating away does not move the app off its own URL. A regression there is treated as a remote-code-execution bug, not a UI bug.