Skip to content
covey

A pipeline should load the configuration of an existing agent into covey after every merge, and a script should fetch the cost of the month's runs on the last day. Both are calls against the same API the web interface uses. The interface signs in with a session cookie set as HttpOnly and SameSite=Strict, and HttpOnly keeps any script from reading it. The documentation describes what happens without a second way in: the work moves beside the product, into somebody's CI, with a copied cookie, or by hand.

The obvious workaround is one person's token with that person's full rights. In July 2024 a story titled "Leaked admin access token to Python, PyPI, and PSF GitHub repos" reached 114 points on Hacker News. One commenter noted that "the secret was never in the repo though. Only in the container image." Another wrote that "This token has been in the wild for 15 months!"

covey answers the same need with an API key. It is created in the browser (under "API keys" on your own profile page) and then travels as the header Authorization: Bearer covey_…. Apart from the exceptions below, every route the interface uses works the same way. The pipeline sends POST /api/v1/agents/{id}/config/import with the agent's bundle as the body.

A key carries exactly the rights of the seat it came from

An account can be a member of several organisations, with a role in each. covey calls that membership a seat. A key is bound to the seat it was created from, and if the person loses that seat the key goes with it: migration 0077 ties the api_keys table to the membership with a cascading delete.

There is deliberately no scope of its own. The documentation argues that a scope which exists only on paper reads like a restriction and enforces nothing, while the role is already checked on every route. An auditor's key therefore reads, and an org admin's key can do whatever an org admin can do. For the cost script an auditor seat is enough, since GET /api/v1/cost/runs is open to every role.

That is also the weak point of the design. Giving the pipeline fewer rights than yourself takes a seat with a narrower role, because a key always reaches as far as its seat.

The example pipeline gets by with an agent_owner seat while a merge leaves the tool allowlists in ACCESS.md and the egress rules in EGRESS.md as the agent has them. If the bundle changes either, covey answers 403; of the roles this route admits, only org_admin can import it. An org admin's key in a pipeline is the same kind of token the Hacker News thread was about.

Creating, rotating and revoking keys and changing the password need the browser

The key routes are wrapped in sessionOnly in server.go and answer a key with 403 and an instruction to sign in in the browser. profile.go makes the same check for the password, while the list of your own keys stays readable with a key.

The documentation gives the reason: a credential that goes astray must not be able to entrench itself. Minting a second key and locking the owner out are the first moves an attacker makes, and in covey both need the password.

The token is shown once at creation; after that the list holds its first characters

A new key gets a name of at most 80 characters and, optionally, a lifetime of up to ten years. The token is shown once, because only its SHA-256 hash is stored. The list keeps the name, the first eight characters after covey_ and a "last used" time that is written at most every five minutes.

Rotation, revocation and expiry apply from the next request

Rotating issues a new token with the same name and seat. Unless a new value is sent, it also keeps the lifetime, counted from now (even if the key had already expired). The old token stops working in the same transaction, so there is no moment with two live tokens for one purpose. Revoking deletes the row and applies from the next request, since every call looks the hash up and checks the expiry date in the same query.

That next request gets 401 "api key invalid or expired" for a revoked or an expired key. A key whose seat is gone gets the same answer, so that whoever probes cannot tell the three cases apart.

Scanners and the API both recognise a key by its covey_ prefix

A commenter under the Hacker News post "My adventure in designing API keys" (April 2026) explained what such a prefix is for. In their words it is meant "for security scanners to detect when they are committed to code / leaked and invalidate them." covey's source code gives the same reason. The API checks the prefix as well: a bearer token without covey_ is not treated as a key at all, and the request ends in 401 "not signed in".

Repositories, open CI variables, URLs and agent sandboxes are the wrong places for a key

The documentation names a repository first, and a CI variable readable by everyone with access to the pipeline. A key also stays out of URLs, because a query string is written into access logs while a header stays out of them. An agent's sandbox is the wrong place too, since the agent reaches covey through the action proxy and needs no credential for it.

The audit log records the person and excludes key management

For changing requests the audit log records the person, their role, method, path and result. The individual key is missing, so which of one person's two keys loaded a configuration stays open. The audit log skips paths under /api/v1/auth/ entirely, including creating, rotating and revoking a key.

For the example pipeline this means one key named after it, with a lifetime. Whether it still uses the key shows up as "last used" in the list.

Back to all posts