ctx.cat Security Model
ctx.cat private shares are zero-knowledge by default: content and metadata are encrypted before upload, and decrypt keys remain in URL fragments.
Signing
The ctx.cat API never signs plaintext user content and never reads local private keys, SSH agents, or GitHub CLI state. The removed /signing/sign endpoint returns 410; no environment variable can restore it.
Author signatures should be created by local clients such as ctxcat or the TypeScript client. Server provenance receipts cover hashes only:
- signature envelope version
- content hash
- metadata hash
- public key fingerprint
- GitHub username claim, if supplied
- author signature timestamp
- backend receipt timestamp
Verification States
unsigned: no author signature.signed: content and metadata match the public-key signature.github-key-verified: signing key appears athttps://github.com/<user>.keys.github-claimed-unverified: the share claims a GitHub user, but the key was not published for that user at verification time.invalid: content, metadata, domain, purpose, or signature verification failed.trace complete: decrypted agent transcript validates and per-record hashes match the encrypted metadata.
Ownership
Private owner URLs use #key=<decryptKey>&owner=<ownerCapability>. Legacy reader URLs using #<decryptKey> still load. The owner capability authorizes encrypted edits, deletion, expiration changes, and owner-marked encrypted comments.
Public and unlisted pages use #owner=<ownerCapability> for edits, deletes, and owner-marked comments. Their content is plaintext by design.
Client-authoritative create capabilities
The v1 create route separates allocation from authorization. A client chooses a random 128-bit record ID and a random 256-bit owner capability. Create sends only a domain-separated SHA-256 commitment over the decoded owner-capability bytes. The raw owner capability is rejected on create and is sent only for an authenticated reconciliation, edit, or delete. Private reader keys never leave the client.
Commitments are authorization verifiers, not bearer credentials. The service stores the commitment in a small immutable authorization sidecar and may store a separate domain-separated create-replay digest so an interrupted create can be retried only with the exact original semantic request. Neither value reveals or replaces the raw capability, and neither is exposed in responses. Logs and errors must contain only the record ID, status, and a generic message; capability and commitment headers must be redacted.
The commitment does not make a weak capability safe. Clients must generate the full 32 random bytes, encode them as canonical unpadded base64url, and compare reconciliation content locally before treating an ambiguous create as success. Private reconciliation streams ciphertext rather than placing base64 in JSON. Clients should hash that stream incrementally, enforce the declared size and a deadline, and compare it with the already prepared ciphertext and encrypted metadata digests before accepting an ambiguous create.
Capability admission and authorization index
Large-record owner headroom is available only after the supplied capability has been verified against its sidecar. A 16-slot process-local credential lane bounds these small verifier reads independently from record-body admission. Missing owner credentials on canonical owner GET, PATCH, and DELETE routes perform no sidecar I/O. Anonymous comments still read the sidecar after anonymous record admission to bind the record and attribution. Invalid credentials never reserve privileged record bytes; canonical reads remain indistinguishable from absence, mutations are rejected, and invalid owner attribution on a comment remains anonymous.
The same admission classification is retained for the serialized mutation reread: an absent or invalid comment capability cannot switch from anonymous to privileged byte headroom merely by reaching the per-record lock.
Startup builds missing sidecars for valid historical records only after private mode reconciliation. Readiness stays unavailable until the bounded index scan is complete and error-free. Startup resumes retained scan slices every 10 milliseconds for up to 58 seconds, within the deployment health deadline, instead of waiting for the 15-minute maintenance interval. Historical sidecar files are individually synced before no-replace publication; readiness stays unavailable until every dirty shard directory is synced at the final index barrier. Sidecars are matched again to every opened record, retained with tombstones, and checked under the per-record lock before a mutation is published. Exact dead-writer pre-link remnants are removed and exact same-inode post-link remnants are reconciled under the shard authority; active or ambiguous evidence is preserved and fails closed. A mismatch is a storage integrity failure, not an authorization fallback.
After readiness has completed once, a normal create, update, or delete that changes the root generation while a retained refresh is open discards that cursor and retries after 10 milliseconds without a prolonged health downgrade. Root-authority changes and corrupt records or sidecars remain fail-closed.
Authorization sidecar occupancy also reserves the ID from multipart initialization. A crash-left sidecar-only create identity and a sidecar retained after physical record deletion cannot be reused by another storage protocol.
This index is not a second writer service. It runs inside the sole API process and uses the same storage-root mutation coordinator as record publication. The deployment trust boundary remains one process, one replica, and no other writer running as the service UID against the live storage root.