The Information reported today, in a story carried by Reuters and Quartz, that Palantir has pressed Anthropic for zero-data-retention guarantees before exposing its models through Palantir software, that Nvidia keeps Anthropic's models to less sensitive tasks and uses its own Nemotron models for internal work, and that Booz Allen Hamilton has barred staff from using Anthropic's commercial model on proprietary cybersecurity work. The trigger, per both accounts, is a June policy change letting Anthropic retain usage logs for 30 days to guard against what it called "complex and novel attacks". Anthropic's reported answer is to let enterprise customers keep activity data in their own cloud storage under their own keys.
Every position in that story is a policy. Zero retention is a promise. Thirty days is a promise. Customer-held keys are better, because the promise becomes "we will not read a store we cannot decrypt", but the customer still trusts that the vendor's software never saw the key on the way in. The stronger property is one a client can check for itself: the service never held the key, and the client notices if the service tries to change that.
shell.online has that property for exactly one thing, so it is worth being precise about which thing.
What the service stores about typed input
A shell.online share puts a live terminal behind a browser link, and someone with an interactive link can type into it. What they type is worth keeping: the command sent to a shell, or the prompt sent to a coding agent. The browser assembles keystrokes into the line actually submitted, applying backspace, Ctrl-U and Ctrl-W and dropping the escape sequences a full-screen program interprets for itself, and emits it on Enter, with Ctrl-C recorded as an interrupt.
Since the change that landed on September 11, each of those lines is encrypted in the browser before it leaves, to a per-organization key pair the code calls the team audit key: ECDH on P-256, HKDF-SHA-256, then AES-256-GCM with a fresh ephemeral sender key per entry. The service keeps the public half, every member's sealed copy of the private half, and the ciphertext. It keeps nothing that opens the ciphertext, so search, CSV export and the audit charts now run in the browser. Each entry's associated data binds the organization, session, entry kind, timestamp, author and key version, so moving one person's command onto another session or person breaks the tag. All of this is in team-crypto.ts in the repository, and the commit that introduced it also says what stays in the clear: session lifecycle entries the service writes itself, carrying session names and addresses it already stores.
The service is the one that says whether a key exists
Sealing alone leaves a hole. The browser learns the team's public key from the service. A service that wanted to read the log has two lies available. It can tell a browser the team has no key yet, in which case the browser's normal job is to generate one and seal the private half to every member the service lists, and the service could put a key of its own on that list. Or it can offer a different public key than the one the team uses, and every entry sealed from then on opens for whoever holds the other half.
Two mechanisms close this, and neither depends on the service being honest.
The first is how the private half travels. A copy of the team key is sealed to a member with the sender's own vault key, not a throwaway one, so the recipient can tell which teammate produced it. A browser opens a share only if the sender is a current member and the vault key it knows for that sender has not changed. A copy the service manufactured has no teammate behind it and does not open.
The second is a 37-line file with fifteen lines of logic. Once a browser has opened a copy and confirmed, by deriving the public point from the private half, that it really is the private half of the key the service published, it records that public key in localStorage under the signed-in user and the organization. From then on, every refresh of the team key runs a verdict function with two inputs, the key this browser has seen and the key on offer, and four outputs:
- create: nothing on offer and nothing ever seen here, so this browser makes one
- use: the key on offer is the one this browser knows, or the first it has seen
- refuse-missing: a key was used here before and the service now reports none
- refuse-changed: the key on offer is not the one this browser used before
The two refusals stop everything. The held key is dropped, nothing new is sealed, and the person sees one of two sentences, verbatim from the source: "Your team has an audit key that this browser has used before, and shell.online is reporting that it has none. Nothing new will be sealed until that is sorted out." Or: "The audit key shell.online reports for your team is not the one this browser has used before. Nothing will be sealed to it. Check with your team before going on."
The service side backs this with deliberately dumb rules. Creating a team key is create-only and returns 409 if one exists. Adding a member's sealed copy is insert-only, must carry the current key version, and is refused unless the caller already holds an opening copy. The only deletion any caller can make is of their own copy. Those handlers are in the app server source, next to the store test showing a second create for the same organization returning false.
What this does not do
It is trust on first use. A browser that has never seen the team's key believes whatever it is told the first time, and the code's own comments say so. A private window trusts afresh every session. Legitimate key replacement does not exist yet: the file says it "will arrive as a new version with a way to say who replaced it, not as a key that quietly differs from the one this browser knows", so a team that genuinely loses its key has no supported path today. History from before sealing is sealed in place by an owner's or admin's browser, and the log labels those entries "sealed later by" and marks the ones still in the clear.
The same hole, named in our own draft
Pilot Protocol has a version of this problem, and our IETF draft names it. In draft-teodor-pilot-protocol-01, each node receives its Ed25519 keypair from the registry on registration, and a receiver verifies a signed key exchange with the sender's public key, "which it obtains from the registry and cross-checks against the claimed Node ID". The security considerations list what a compromised registry could do, and the third item is "public key substitution (enabling identity impersonation)". The registry is the trust anchor for identity there, as the shell.online service is for the team key's public half.
The team-trust module is the shape of a client-side answer: remember the key you have already proven, refuse a quiet change, refuse a claim of absence, and make replacement an explicit, attributed event when it exists. For an overlay network of agents whose draft already allows identity to be persisted to disk across restarts, that persisted record is the natural place to pin peers' keys too. That is design direction, not shipped code, and the Pilot Protocol source is where to check which it is.
A retention policy tells you what a vendor intends. A pinned key tells you what a vendor can do. Palantir is asking for the first because the second is not on offer for a hosted model. For a log of what a person typed into a terminal it is, and the price is fifteen lines of logic and two refusal messages a person can act on.
Top comments (0)