Privacy
Privacy policy
Counterseal holds your most sensitive keys and runs a public log, so we collect as little as we can, say exactly what that is, and say exactly what is public.
The short version
- No passwords, no email address required, no tracking, no analytics, no ads, no third-party scripts.
- Your face or fingerprint never reaches us. We only ever see a public key and a signature.
- API keys you vault are encrypted. No API returns them, and decrypting one needs the master key, which only the running service uses.
- One cookie: your sign-in session. The app also installs a service worker that keeps copies of the app's own files (its page, script, styles, fonts and icons, never your data) so it can open without a network, and remembers in this browser whether you hid its Get started list and whether it already said it was installed. If you turn on alerts, your browser also keeps a push subscription. The public log page remembers the last checkpoint it showed you, in this browser only.
- If receipts are on, a short record of each decision is published in a public log, permanently. It never contains a path, body, key, token, name, email or IP address. See What is public.
- You can delete your account and everything we hold about you yourself, at any time, in the app. Public log entries stay, but nothing on our side links them to you any more.
Face ID and passkeys
When you sign in or approve a held call, Counterseal uses a passkey (the WebAuthn standard). Your phone or laptop asks you for Face ID, Touch ID, Windows Hello, a fingerprint or your device PIN. That check happens on your device.
If it passes, your device signs a one-time challenge from us with a private key kept by your device or your passkey manager, never by us. We receive the signature and check it against the public key you registered. We never receive, store or have any way to see your face, fingerprint, PIN or private key.
The demos on our homepage
Every demo on our homepage runs only in your browser. The live refund at the top, the signature attempts and the log you can try to rewrite use keys your browser makes for that page and then throws away. Nothing you do there is sent to us or stored. "Approve with passkey" in a demo is a stand-in button: it does not use your passkey, Face ID or camera. The homepage makes two requests to us: it loads the public list of irreversible calls (/v1/meta) and reads the public log's latest signed checkpoint (/v1/stats). Its fonts, images and videos come from this site; nothing loads from anywhere else.
What we store
| Data | Why |
|---|---|
| The name you type at sign-up | So the app can greet you. It can be anything. It is never in a public receipt. |
| Your passkeys' public keys, a usage counter, and the host (RP ID) each was made for | To verify sign-ins and approvals, and to detect cloned authenticators. A passkey works only on counterseal.gautamkhosla.com. |
| Move links (only accounts that used the retired move from leash.gautamkhosla.com) | A hash of the one-time link, your account, and when it was made, used and expired. No new ones are made. Deleted a day after expiry, and with your account. |
| API keys you add to the vault, encrypted | To call the provider on your agent's behalf. Each key has its own AES-256-GCM key, wrapped by a master key held outside the database. We keep the last four characters in clear so you can tell keys apart. |
For a Shopify key, the store address you give (your-store.myshopify.com) | So the key is sent only to that store. It is stored next to the encrypted key, shown to you in the app and in your private audit log, and never in a public receipt. |
| Agent tokens and receipts API keys, as hashes | To recognise your agents. The token or key itself is shown to you once and never stored. |
| Agent public keys (optional) | The public half of each agent key you register, its label and when it was last used, to check your agents' signatures. Never a private key: we refuse any key that contains one. |
| Signature nonces | A random value from each signed agent request, with the key's fingerprint, so a signed request cannot be replayed. Deleted once the signature has expired (minutes). |
| Held calls: method, host, path, rule, time, decision, and a preview of the request (query string and up to 2 KB of the body or SQL). For a Stripe refund or payout held by a money cap, the preview starts with its amount, currency and charge, and the day's total so far | So you can see exactly what you are approving. The preview is deleted a day after you decide; the audit log keeps only a fingerprint (hash) of it. |
| Guard keys, as hashes, with their label and when each was made and last used | So the guard on a coding agent's machine can ask you for approvals. The key itself is shown to you once and never stored. |
| Held commands from a coding agent's guard: the exact command (or tool call), the guard's short summary, the agent's name, the folder's name (never its full path), the agent's session id, the rule, the time and the decision, and the reason you give if you deny | So you can see exactly what you are approving, and so the agent learns why you said no. A command can contain anything the agent typed, including a secret in a connection string. The guard on your machine removes the credentials it recognises (passwords in connection strings, bearer tokens, key-shaped values, PASSWORD= style variables) before it sends a command, but it cannot recognise every secret. The exact command is deleted a day after you decide; the audit log keeps only a fingerprint (hash) of it, and your deny reason. Held commands are shown on your devices and may appear on the lock screen. |
| Break-glass windows (an owner or approver opens one): who opened it, when, for how long, which token or guard key it covers (or all), and the reason they typed (up to 200 characters) | To let held calls through for an hour at most, and to tell the whole team who did it and why. Everyone on the team sees it; it is never in a public receipt (a call it let through says only that it was let through by break glass). Kept for a week after it ends, then deleted. |
| Alert channels (Slack, Discord or Google Chat) an owner connects: the service, the name they gave it, the host of its address, and the address itself, encrypted with the same key as your vault and never shown again | To send the team's alerts there. What is sent: who asked (a token, agent or gate name), the rule, how many seals, and a link; never the path, command, body, amount, a person's name or a window's reason. Slack, Discord or Google receive that text. Deleting a channel or the account deletes the address. |
| Deploy gates you make: a label, the repository's name, the workflow files, branches or tags, environments and triggers you list, whether its public receipts may show the repository's name, the key it hands out and that key's policy (if you chose one), and, after its first approved run, GitHub's numbers for the repository and its owner | To decide which GitHub Actions jobs may ask you to approve a deploy, and to refuse a repository that was deleted and re-created under the same name. Kept until you delete your account. Revoked gates are kept, marked as such. |
| Held deploys (a GitHub Actions job waiting at one of your gates): the repository, branch or tag, commit, workflow, run, environment and trigger, the GitHub login of whoever started the run (all from the token GitHub signed for the job), the one-line summary the workflow sent, the rule, the time and the decision, and the reason you give if you deny. A secret each waiting job polls with (as a hash) and the id of each GitHub token a gate accepted | So you can see exactly what you are approving, and so the job's log shows why you said no. The login, the summary and the rest of the facts are deleted about a day after you decide; your audit log keeps only the repository, commit, branch or tag, run and gate (never the person or the summary), and your deny reason. Poll secrets are deleted when they end; token ids when the token could no longer be used (minutes). A deploy alert on your devices shows the gate's label, the repository, the commit and the environment or branch, and may appear on the lock screen. |
| Your audit log | Writes, holds and denials (with the method, host and the first 300 characters of the path; for a held command, only a fingerprint of it and your deny reason), approvals (with a short fingerprint of the passkey that approved), token and key changes, hash-chained so tampering is detectable, and the index of each entry's public receipt. |
| Brakes: the limits and money caps you set on a token (part of its policy), how much each token's money caps have let through on each UTC day (currency and total), the breaker's counts, and whether your account is frozen and since when | To enforce what you set. Daily totals are kept 35 days. The breaker keeps a count of a token's writes in the current hour, and a count per exact request in the current 5 minutes, identified by a short prefix of a hash of the request, never its content; they are deleted when their window ends. For a Stripe call a money cap let through, your audit log also keeps the amount, currency and charge or payment intent id. |
| On a team (if an owner invites people): each person's name (as they type it when they join), their role, the label the owner gave their invite, who invited them, when they joined, and when their approvals start to count; each invite as a hash of its link, with its role, label, who made it and when it expires or was used; and each seal on a held action: who gave it, a short fingerprint of their passkey, and when | So two different people can approve one action, and so the team can see who did what. Everyone on the team sees the team, the holds, the audit log and the command line sign-ins, including where each started. An invite link is shown once to the owner who made it and stored only as a hash; it works once, for up to 24 hours. Seals are deleted two days after they were given (the audit log keeps them); invites a day after they expire. Removing someone deletes their sessions, passkeys and push devices at once (their devices get one last wake-up, which then reads that they are signed out) and revokes what they made; their name stays in the audit log. |
| Receipt openings (if receipts are on) | For each of your public entries, a random salt and the id of the key that sent it, kept with the log so only you can prove the entry is yours. |
| Receipts counters (if you use receipts) | How many receipts your account recorded this month and today, and how many decisions could not be sealed, to enforce the limits. Kept for about 13 months. Replay records of signed receipts: 30 minutes. |
| When your account was last used (a time, updated at most once a day) | Only to count how many accounts were active in the last week, as a total. Nothing about what you did. |
| Five first-time dates: when your account first made an agent token, first had a call go through Counterseal, first tried the demo held call, first opened the installed app, and first turned on alerts | To show you a Get started list that ticks itself, and to count, as totals per day, how many accounts reach each step. Each is written once and never updated. Nothing about what the call was. |
| Demo held calls (if you press Try a held call) | A pretend request the app makes so you can try an approval. It is stored and kept like a held call, but nothing is sent to any provider and nothing goes to the public log; your audit log says it was a demo. |
| Session records, as a hash | To keep you signed in. Web sessions expire after 12 hours and command line (CLI) sessions after 7 days; you can revoke a CLI session in the app. |
| Where each command line sign-in started, coarse: the country and the network's name (for example "Deutsche Telekom") and number, as Cloudflare reports them. Never the IP address. | So the approval screen can show where a counterseal login started next to where you are (a sign-in from another country is a warning sign), and so you can review and revoke command line sign-ins. While you approve, your own browser's country and network are compared with it and not stored. Kept with the pending code (5 minutes; the network number only there) and with the CLI session it becomes (7 days at most). The country and network name of each sign-in, approval or denial also stay in your audit log. |
| Push addresses of devices where you turned on alerts (optional) | To wake those devices when a call is held, a command line is signed in to your account, or your team changes. On a team, each device belongs to the person who turned alerts on, and every member's devices are woken. The push carries no content, so Google, Apple, Mozilla or Microsoft (whoever runs your browser's push service) learn only that a wake-up was sent, never why. Your device then fetches the alert text (the method, host and path of the held call and, for a held Stripe refund or payout, its amount, and how many approvals it has; the guard's summary of a held command; the country and network a sign-in came from; that your account is frozen; or who joined, left or changed role on your team, and when protection will be lowered) from us over your own session; it is shown on your device and may appear on its lock screen. |
| Push addresses of devices where you turned on alerts (optional) | To wake those devices when a call is held or a command line is signed in to your account. The push carries no content, so Google, Apple, Mozilla or Microsoft (whoever runs your browser's push service) learn only that a wake-up was sent, never why. Your device then fetches the alert text (the method, host and path of the held call and, for a held Stripe refund or payout, its amount; the guard's summary of a held command; the gate's label, repository, commit and environment or branch for a held deploy; the country and network a sign-in came from; or that your account is frozen) from us over your own session; it is shown on your device and may appear on its lock screen. |
| A salted hash of your IP address (IPv6 grouped by /64) | Only for rate limiting, deleted after the limit window ends (a minute; an hour for command line sign-in starts; a day for the sign-up limit). Some limits are kept only in memory and never stored. |
What is public
If receipts are on for your account, each decision the broker makes on a write (an allowed write, a hold, an approval, a denial, an approved call going through) is published in the public log, permanently: anyone can read it, and it cannot be removed, because that is what makes the log trustworthy. A broker entry contains only:
- the method and the provider's API host (for example
DELETE api.github.com), or for a coding agent's held command onlyRUNorCALLand the agent's name from a fixed list (for exampleRUN claude-code, never the command, its folder or your key's label), the decision, the rule id and the map version. For Shopify, which sends calls to your own store, the host word is onlymyshopify.com: your store address is never public; - whether a registered agent key signed the request, and whether a passkey approved it (yes or no, never which one); when a team requires two approvals, how many were required and how many were given (numbers only, never who);
- the method and the provider's API host (for example
DELETE api.github.com), or for a coding agent's held command onlyRUNorCALLand the agent's name from a fixed list (for exampleRUN claude-code, never the command, its folder or your key's label), or for a deploy gate onlyDEPLOY github.com(never the commit, workflow, run, person, the gate's label or the workflow's summary; the repository's name appears only if you chose that for the gate when you made it, and then it stays in the log for good), the decision, the rule id and the map version. For Shopify, which sends calls to your own store, the host word is onlymyshopify.com: your store address is never public; - whether a registered agent key signed the request, and whether a passkey approved it (yes or no, never which one);
- the time, and the SHA-256 hash of your own audit entry for that decision;
- a salted commitment to your account, different for every entry, so entries cannot be linked to you or to each other.
The time of each decision is public. Someone who already knows when you did something (for example, that a repository disappeared at 14:03) could guess which receipt is yours, but could not learn anything else from it.
Never the path, query string, body, response, key, token, account id, name, email, IP address, amount or currency: a Stripe refund or payout that a money cap let through appears as the method, the host, the decision and the rule id (st.money:capped), and a breaker that tripped as the rule id brake.tripped. Freezing, unfreezing and resetting a breaker are in your private audit log only. Receipts that your agents send themselves with a receipts API key are public too, with exactly the fields they send: send hashes, not content.
New accounts start with receipts on, and the sign-up screen says so. Accounts made before receipts existed have them off until their owner turns them on. You can turn them off at any time with your passkey; entries already published stay.
What we never store
- Your biometric data, device PIN or any private key.
- Your API keys in readable form, anywhere: not in the database, not in logs, not in error messages.
- The bodies of requests your agents send or the responses they get back. They pass through and are gone. The only exceptions are the short preview of a held call, the exact text of a held command and the facts of a held deploy, kept until about a day after you decide on it. When a money cap checks a refund, it reads what Stripe says about the charge (its currency and what is left) and keeps none of the rest.
- Your IP address in clear, your precise location, or any device fingerprint, in our database. The only location we keep is the country (and network name) a command line sign-in started from, described above.
- What you paste into the receipt verifier or the signature checker: both run in your browser. The checker sends one request to us only if you ask it to fetch an agent's key directory, and that request carries only the agent's origin.
Where it lives
Counterseal answers at counterseal.gautamkhosla.com. Its earlier address, leash.gautamkhosla.com, only redirects there (HTTP 301) and serves nothing itself. It runs on Cloudflare Workers, Cloudflare D1 (accounts, vault, holds, audit) and a Cloudflare Durable Object (the public log). Cloudflare is our only processor. If you turn on alerts, your browser's push service delivers an empty wake-up message. Requests are handled at the Cloudflare location nearest to you; the database lives in one Cloudflare region.
Workers Logs are turned off for this service, so we do not keep request logs or our own log lines. Cloudflare itself may still process and briefly keep request metadata, such as IP addresses, to run and protect its network, under its own privacy policy. We never write your keys, tokens or request bodies to any log.
How long we keep it
- Sign-in challenges and device login codes: minutes. Rate limit records: until the window ends. Signature nonces: until the signature expires. Signed-receipt replay records: 30 minutes.
- Sessions: until they expire (12 hours on the web, 7 days for the command line), you sign out or you revoke them.
- Vault keys: until you delete them. Tokens, receipts keys, agent keys and guard keys: until you delete your account; revoked or expired ones are kept, marked as such, for your audit trail.
- Held calls and commands: deleted 365 days after they close, or 90 or 30 if your account's owner chooses that in the app (Privacy). Their preview text goes a day after they close, as above. Waiting ones are never deleted early.
- Audit log and receipt openings: until you delete your account. The audit log is hash-chained, so deleting old entries would break the proof that nothing was removed; download the evidence pack to keep your own copy. Daily money totals: 35 days. Breaker counts: until their hour or 5 minutes ends.
- Team members: until they are removed (their sessions, passkeys and push devices go at once) or the account is deleted. Seals on held actions: two days. Invites: a day after they expire.
- Holds, audit log and receipt openings: until you delete your account (the preview of a held request, and the facts of a held deploy, about a day after you decide). Deploy gates: until you delete your account. Daily money totals: 35 days. Breaker counts: until their hour or 5 minutes ends.
- Public log entries: permanently.
Expired rows are refused as soon as they expire, and are deleted by a cleanup that runs during normal traffic, so a deletion can lag behind when the service is quiet.
Your rights
You can see everything we hold about you in the app, and an owner can download all of it as one file (below): your vault, tokens, agent keys, receipts keys, holds, audit log, receipts and team. You can delete any key or token you made at any time (on a team, an owner can delete anyone's), and leave a team yourself. Delete account in the app (an owner, on a team) erases the account, its members, invites and seals, passkeys, vault, tokens (with their money totals and breaker counts), agent keys, receipts keys and counters, guard keys, holds, sessions, push devices, rate limit counters and audit log immediately, and erases the openings of your public entries, so nothing on our side can say whose they were. The public entries themselves stay, and the log keeps a one-way hash of your deleted account id so that a receipt already in flight is unlinked too. Database backups kept by Cloudflare roll off within 30 days.
Wherever you live, including under GDPR, PIPEDA or CCPA, you can also ask us to access, correct, export or delete your data by writing to us. We do not sell or share personal data.
Download your data
In the app, under Privacy, an account's owner can download everything Counterseal keeps about the account as one JSON file: the account and its settings, its people and roles, passkeys (as short fingerprints), sessions (when and where from, never their secret), vault entries (provider, label, store address, dates; never the key or any part of it), tokens and their policies, agent keys, receipts keys and guard keys (labels and dates, never the key), every held call and command still kept, seals, invites, which push services your alert devices use (never the push address), receipts counters, money totals and the whole audit log. It needs a fresh passkey confirmation, works 3 times an hour, and is itself written to the audit log. The held calls and the audit log come in pages, so the download works for long histories.
Someone else on a team who wants their own data can ask the account's owner, or write to us.
Records of processing (summary)
What a record of processing activities would list for Counterseal. The account's owner decides why their agents use it; for the data about their team and their agents' requests, we act on their behalf.
| Purpose | Data | Basis (GDPR Art. 6(1)) | Kept |
|---|---|---|---|
| Accounts and sign-in | Name, passkey public keys, sessions, coarse place of command line sign-ins | (b) contract | Until deletion; sessions 12 hours or 7 days |
| Holding and approving irreversible calls | Held requests and commands, previews, decisions, seals, deny reasons | (b) contract | 365 days after they close (30 or 90 if chosen); previews 1 day |
| The vault | Your API keys, encrypted | (b) contract | Until you delete them |
| Proof: the audit log and public receipts | Hash-chained decisions; public receipts without personal data | (b) contract; (f) legitimate interest in tamper evidence | Audit log until deletion; public entries forever, unlinked on deletion |
| Alerts | Push addresses | (a) consent: you turn them on | Until you turn them off or delete the account |
| Abuse prevention | Salted hash of the IP address's network | (f) legitimate interest in keeping the service up | Until the limit window ends (a day at most) |
| Launch numbers | Last-used day, five first-time dates | (f) legitimate interest in knowing whether the product works, as totals only | Until deletion |
No automated decisions with legal effect: Counterseal holds calls by fixed rules you can read, and only a person approves. Security measures: see the security page and the security standards self-assessment.
Processors and the DPA
Our only processor (a subprocessor, when we act for a business) is Cloudflare. What it does and where is on the subprocessors page. Businesses that need a data processing agreement can start from our DPA template; it is a template, not legal advice, and both sides' lawyers should review it.
Contact
Counterseal is run by Gautam Khosla in Ottawa, Canada. Privacy questions: privacy@gautamkhosla.com. Security reports: see our security page.