Security model
Identity and tenancy
Production account identity comes from Clerk. Clerk sessions are verified on the
server and restricted to the configured Cohent origin. GitHub authorization is a
separate repository-access connection; signing into Cohent does not by itself grant
access to a repository. GitHub authorization uses hashed, single-use state, PKCE S256,
encrypted expiring access/refresh tokens, hashed
web sessions, HttpOnly cookies, same-origin checks, and double-submit CSRF
validated against the server-side session. The local synthetic identity is
guarded by NODE_ENV !== "production". Project membership is checked on the
server for every snapshot and action.
GitHub-derived membership is the intersection of the user's repository permission and the App installation. It is reverified periodically and revoked on installation, repository, or authorization webhooks. Manually invited membership is never silently overwritten by the GitHub projection.
Roles:
- owner: full project and invite authority
- admin: invite and collaboration administration
- member: edit, launch, message, and review
- viewer: read, presence, and room chat
Invitations
Invite links contain 256 bits of random token material and only a SHA-256 hash is stored. Every invite points to exactly one repository project and expires after seven days. Email invitations are one-use and bound to the recipient's normalized account email. A copied team link allows up to 25 distinct accounts; creating a replacement team link revokes the previous active team link for that project. Already-joined members can follow an old link back to the correct project without consuming it again.
Projects without a connected repository are private setup spaces and cannot be shared. Connecting a repository creates or opens its own project, with separate members, invitations, room, agents, notifications, and scope.
Input and concurrency
The route parses a closed action union, rejects unknown actions and enum values, bounds persisted text, and verifies that referenced tasks and agents belong to the current room/project. Shared brief writes use a revision predicate.
JSON writes require a non-simple content type, while platform auth uses
same-origin identity. GitHub sessions additionally require X-Cohent-CSRF.
Webhook bodies are bounded to 2 MB, HMAC-verified with SHA-256, and delivery IDs
are idempotent. Production should additionally set a restrictive CSP and alert
on repeated authorization failures.
The Worker applies a broad edge request limit and a tighter limit to invitation, bootstrap, account, and GitHub authorization entry points. D1 also enforces durable per-user, per-project limits for writes, presence, invite creation, email delivery, and invite acceptance. Cloudflare's automatic DDoS protection remains the outer network layer; WAF rate-limiting rules should be enabled for sign-in and API abuse.
Agent prompts and checkpoint summaries are redacted server-side: the adapter
sends the prompt as written and protectPrompt runs before storage, so a secret
in a prompt does leave the developer machine over TLS and is scrubbed before it is
persisted or shown. They are normalized, redacted, and only then bounded, so a
credential straddling the stored bound is removed rather than clipped. Redaction scans the first 8000 characters, so a credential that begins
beyond that window is dropped by the bound rather than matched, and a multi-line
key whose terminator falls outside it can still be stored truncated.
Project policy controls whether full prompts are visible to admins, members, or
nobody; summary-only views never expose hidden prompt text. Retention replaces
expired prompt and summary content while preserving non-sensitive audit
metadata. GitHub tokens and raw agent pairing tokens never enter room events.
Repository adapter
Git never executes Cohent during clone or pull. The repository ships declarative instructions and command hooks that supported clients review under their own project-trust controls.
Device and agent credentials:
- are random bearer credentials scoped to one person, device, and repository;
- are stored by Cohent only as SHA-256 hashes;
- use a 90-day device credential to mint short-lived 24-hour agent credentials;
- live in the user-level
~/.config/cohent/credentials.json, outside the repository; - can be revoked with
disconnectfor one repository orlogoutfor every repository; - are never written to shared runtime context, prompts, or Git history.
A write to a path outside the agent's declared scope is refused. Scope is what gets compared against other agents' claims, so a write to an undeclared path has been checked against nothing — allowing it silently meant collision detection could be bypassed by omission rather than by intent. This covers tools whose payload names a path; an unrecognised tool shape is treated as unknown rather than as a violation, so the hole that remains is narrower and named rather than pretended away.
The adapter treats peer prompts and checkpoints as untrusted data. Injected
context cannot override the current user request, repository rules, permission
policy, or higher-priority system instructions. The write hook covers edit tools
and shell tools: edit tools are held to the full protocol, shell tools only to a
pause, a cancel, an unreachable control plane, or an unusable configuration. Shell
coverage includes the tools that drive an already-open session (unified_exec,
write_stdin, BashOutput), because the command that opened that session was
approved before the cancellation and nothing re-checks it otherwise. MCP tools are
treated the same way: the kill switch reaches them, the scope rules cannot, because
their arguments are defined by a server outside this repository.
The adapter's own command is exempt from the kill switch — scope is declared by
running it — so that exemption is matched end to end rather than by substring. The
interpreter is fixed, the script path must be a real .cohent/cohent.mjs, and an
environment prefix is limited to COHENT_* names with inert values, because a general
prefix (NODE_OPTIONS=--require=…, PATH=/tmp/evil:$PATH) executes attacker-chosen
code before the adapter starts, through the one path designed to be unblockable.
It is a coordination guardrail, not a security sandbox — it binds only a paired, cooperating client, and branch protection, tests, review, and runner isolation remain authoritative.
A hook only guards what the client runs. Measured on Codex CLI v0.142.5
(2026-08-10) and v0.147.0 (2026-08-11): a non-interactive codex exec executed shell
commands while no hook fired at all — in the shipped config format, in the snake_case
format the binary's config parser names, and with hooks injected via session flags. The
interactive TUI explains why. A project's hooks sit behind two human gates: a directory
trust dialog, and a hook review screen at session start ("N hooks need review before
they can run", "Press t to trust all"). Once a person presses t, hooks do fire — in
the TUI and in exec — and a PreToolUse deny genuinely blocks the tool, verified by a
denied touch whose target was never created.
Codex enforcement is therefore real and gated on a one-time human trust step. The
trust is recorded in $CODEX_HOME/config.toml as
[hooks.state."<hooks.json path>:<event>:<group>:<hook>"] with a trusted_hash, so
editing .codex/hooks.json — including an edit Cohent ships — revokes it, and the next
session runs unguarded with nothing to notice. node .cohent/cohent.mjs doctor reports the
trust state for the checkout for that reason. codex exec --dangerously-bypass-hook-trust makes hooks fire without the gate; Cohent does not
recommend it, because that gate is what stops a cloned repository from running its own
code on a developer's machine.
node .cohent/cohent.mjs doctor verifies more than that a trust record exists. Codex keys
each record by hooks-file path, event, group index and hook index, so the recorded set is
compared against the hooks the file actually declares and a hook with no record is named
rather than averaged into a count. An edit that changes a command in place changes no
index, so doctor additionally warns when .codex/hooks.json is newer than Codex's
trust store. It cannot verify the trusted_hash itself — the preimage is undocumented —
and says so rather than implying coverage it does not have.
Cohent reports the residual condition rather than assuming it away. A lease belongs to a
client session, not to the pairing token: a token identifies a person for thirty days
and is shared by every session they run, so token-scoped renewal allowed one live poll to
refresh the lease of every intent that token ever announced, and an abandoned agent read
as reachable. Each intent records its session, and lease renewal, control-command
delivery and acknowledgement all match on it — strictly in both directions, so a session-less caller can
neither renew nor collect for a session-aware intent. A command aimed at a session that
never returns stays pending until it expires, which is what stale is warning about.
Every contact with the control plane grants a work intent a 90-second lease; the room publishes live
while it holds, idle once it lapses but the agent was seen inside the ten-minute
control-command lifetime, and stale beyond that, where a command issued now would
expire undelivered. An intent published without a pairing token is unpaired and
offers no controls at all, and a room that is archived offers none either. The previous
single ten-minute window answered a question about the agent with the deadline of the
command, so a crashed agent read as reachable for ten minutes.
Before every paired write, the adapter refreshes peer collision state and targeted owner controls. A pause or cancel is persisted locally before it is acknowledged. A resume clears that state. If the control plane cannot be reached, paired writes fail closed instead of proceeding with stale authority. Commands are isolated to the exact hashed pairing-token record, expire after ten minutes, and use replay-safe acknowledgement states.
Runner isolation
The control plane never executes arbitrary shell input. No runner worker exists today — runs are simulated in-process and touch no repository — so the following are requirements for a real implementation, not current behaviour:
- one worktree or sandbox per task;
- repository and command allowlists;
- least-privilege permission profiles;
- outbound network policy;
- job-scoped secrets;
- CPU, memory, disk, and time quotas;
- structured logs with secret redaction;
- cleanup and artifact-retention policies.
No runner output is trusted merely because an AI produced it. Required checks, human review, GitHub branch protection, and merge policies remain authoritative.