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 disconnect for one repository or logout for 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.

CohentPractical guides for using and understanding Cohent