Table of Contents
- The seven signals
- How the awkward three actually work
- Terminal commands observes process creation, not your shell
- Typed text samples a field at a boundary, and there is no keystroke buffer
- Clipboard keeps a fingerprint, and the fingerprint is content
- Timing, and why some signals go quiet
- What state each signal can be in
- Platform support
The seven signals
Each signal is an independent process with its own manifest, its own grant, and its own event type. Nothing is captured until the person switches that signal on.
The "does" and "does not" columns below are quoted from the manifests themselves
(collectors/*/collector.manifest.json), not paraphrased. The same strings are what the app
shows the person before they grant anything, which is the point of keeping them in the manifest
rather than in a UI file: the promise and the explanation are the same string.
| Signal | Does | Does not |
|---|---|---|
Window activity (focus) |
records which app and window you are working in, and for how long | read the contents of those windows |
Typing rhythm (input.metrics) |
counts keystrokes, clicks and scrolling, and how fast you type | record which keys you press or what you type |
Screenshots (frames) |
takes a picture of the one window you are working in when its contents change | capture your whole screen, other windows, or apps you have denied |
Terminal commands (shell) |
records the commands you run in a terminal, and whether they succeeded | record command output, built-ins like cd, or a command containing a detected secret |
Typed text (text.commit) |
records text you finish typing into a field in an allowed app | record key by key, anything in a password field, or the text of a long document |
Clipboard (clipboard) |
records that you copied something, its size, and a one-way fingerprint | store what you copied |
Websites (browser) |
records which website you are on, as a host name like github.com |
record the page address, page contents, what you type into it, or anything from a private window |
How the awkward three actually work
Four of these are straightforward. Three needed a design decision to exist at all without being a keylogger, and those decisions are the interesting part.
Terminal commands observes process creation, not your shell
It watches for new processes and reads the command line from the process itself. It never reads shell history — the manifest forbids it, and history would hand over commands you ran before this was installed and before you consented.
The consequences are stated rather than hidden: it cannot see built-ins (cd, export,
anything the shell handles without spawning), and it attributes the command to the shell
process, not to whichever window happened to be in front.
Typed text samples a field at a boundary, and there is no keystroke buffer
It reads the focused field's value at a boundary — when focus leaves the field, or when the field empties while still focused, which is what "you pressed send" looks like from outside. Nothing accumulates key events anywhere; there is no buffer to leak, subpoena, or accidentally log.
Password fields are never sampled, and that check fails closed. Anything over about two thousand characters reports its length only, because a long document is not a "typed text" event, it is the document.
Clipboard keeps a fingerprint, and the fingerprint is content
It polls a sequence number to notice a copy happened, then stores the format, the length, and a keyed HMAC. Keyed, not a plain hash — an unkeyed hash of a short string is a dictionary attack away from the string.
And the fingerprint is marked as content, which matters more than it sounds: without that, suppression would strip the text but keep the hash, and a denied app's copies would stay linkable to an allowed app's. A test caught that it originally was not marked.
Timing, and why some signals go quiet
input.metrics reports on a 15-second window. The others report on events. The gate's staleness
window for deciding a collector is dead is 45 seconds — three of those windows — because an
idle machine produces no events, and an event-driven heartbeat made a perfectly healthy
collector look dead.
What state each signal can be in
The console shows one row per signal, and it distinguishes cases that all look like "off":
| State | Means |
|---|---|
enabled_active |
granted, running, and producing |
enabled_idle |
granted and running; nothing has happened |
denied_no_grant |
the person has not switched it on |
not_implemented |
declared in the contracts, no shipping code — "not built", not "broken" |
collector_not_running |
built and granted, but the process is not up — "not connected" |
refused |
this machine cannot honour it; the reason names the setting to change |
error |
it tried and failed |
Telling not_implemented from collector_not_running was a real bug: the gate reused one state
for both and the console called everything "NOT BUILT", including signals that were merely
paused. Two different problems that need two different actions from the person should not share
a word.
Platform support
All seven have working bodies on both Windows and macOS. See Porting and platform support for what each OS can and cannot honour, and which promises get weaker where.
here-telemetry
How it works
Working on it
Taking it further
In the repo: README.md for the
invariants, HANDOFF.md for
current state.