Configure a repo
What .cujo.yml accepts, which branch each key is read from, and why that is not the same branch.
The whole file
Optional, and every key in it is optional. Missing keys are inferred from the repository’s own build files — pyproject.toml, package.json, a Makefile, a CI workflow. Write only what Cujo got wrong.
mode: diff
install: uv sync
test: uv run pytest
boot: uv run uvicorn app:app --port 8000
smoke:
- GET /health
- GET /orders/1
allow_hosts:
- api.stripe.com
discord_guild: "222222222222222222"Two readers, and two branches
Six of these keys are read from the pull request’s base tree — five by the agent in the sandbox, and mode by the service before there is a sandbox. The seventh is read by the service from the repository’s default branch. None is read from the pull request’s own code.
That is the point of the whole design. Policy that a pull request could edit is not policy: a change could add its own exfiltration host to allow_hosts in the same commit that calls it. And a declaration that has to be merged is proof that whoever made it controls the repository, which is what makes it worth anything.
.cujo.yml, the run records a warn saying so and uses the base version anyway. The edit takes effect once it is merged, like any other policy change.The keys
| Key | Read from | Meaning |
|---|---|---|
| mode | base | Which review a pull request gets: sandbox runs it (the four checks below), and diff reads it against the repository’s own standards files on a cheap model, with no sandbox and findings of at most warn. Absent means the instance’s default, which is sandbox unless the operator changed it. Two floors override either answer: a pull request that changes a dependency manifest, or one a Bot account opened, is always the sandbox. |
| install | base | How to install the repository. Inferred when absent. |
| test | base | How to run the suite. If it is absent and cannot be inferred, the run stops with one warn — “no test suite found” — and no checks are spawned at all. |
| boot | base | How to start the app, for the smoke check. |
| smoke | base | Endpoints to hit once it is up, each written METHOD /path. |
| allow_hosts | base | Hosts this repository legitimately reaches. Anything contacted that is neither listed here nor a known package index counts as unknown egress. This is the one key that appears in no build file, so it is the one key that can never be inferred — if your build talks to a host, only you can say so. |
| discord_guild | default branch | Which Discord server may receive this repository’s cards, as a quoted id. Half of a two-part binding; see Discord notifications. |
mode and discord_guild are extracted by a single strict line match rather than parsed as YAML, so a malformed value is not an error — it simply means the repository has declared nothing. A configuration typo must never cost a review.
When the file cannot be read
Four outcomes, and they are not the same outcome:
read— the policy is used.absent— there is no file. Everything is inferred, and no host is allowlisted.too_large— over the size cap. The agent reads the file itself rather than proceeding on half a policy.unreadable— a symlink pointing out of the checkout, or an I/O error. The run stops and says so. Half a policy is worse than none, because the missing half is usually the allowlist.