← All posts

GUIDE

One set of rules for Claude Code, Codex and Cursor on your team

Every agent has its own settings format, and every developer configures it differently. How to commit one policy to a repository, what a repository may and may not change, and how to check what the agents did.

On most teams, people do not use the same coding agent. One developer uses Claude Code, another Codex, a third Cursor. Each agent has its own permission settings, in its own format, in each person’s home directory.

So when the team agrees that “agents never touch the production deploy config”, there is no single place to write that down. You write it three times, in three formats, on every machine, and hope nobody’s copy drifts.

I build OpenMoat. This post shows how one file in the repository gives everyone the same rules, whatever agent they use, and what that file is not allowed to do.

One policy, three agents

OpenMoat checks each agent’s actions through its hooks, against one policy in one format. Your own policy lives in ~/.moat/policy.yaml. A repository can add its own in .moat/policy.yaml at its root:

version: 1
deny:
- id: no-prod-config
reason: production settings stay with the release pipeline
fs.write: ["${project}/deploy/prod/**"]
ask:
- id: migrations
reason: database migrations change shared data
shell: ["npm run migrate*"]

Commit it. From then on, everyone who works in the repository gets those rules, whether their agent is Claude Code, Codex or Cursor. A blocked call shows the rule as repo:<id>, so the developer sees it came from the repository:

moat: deny [repo:no-prod-config] — production settings stay with the release pipeline

The reason is what the agent and the developer read. Write it for them.

A repository can only make things stricter

There is a catch, and it is on purpose. A repository is untrusted input. You clone strangers’ code, and a pull request can change any file in it. If a committed policy could allow things, a malicious repository could ship a .moat/policy.yaml that allows reading your SSH key.

So on its own, a repository policy adds only deny and ask rules. Its allow rules apply only after a person reviews and trusts that exact file, in that checkout:

Terminal window
moat trust # in the repository; prints the allow rules it lets in
moat trust --revoke # take it back

Any later change to the file drops it back to deny and ask only, until you trust it again. A pull request that quietly adds an allow rule does nothing until someone looks at it.

Two more safety rules:

  • A repository policy that does not parse denies every call in the project until it is fixed. A broken file never fails open.
  • Defaults, executables and sandbox settings stay in each person’s own policy. A repository policy that tries to set them also denies every call in the project.

Test the policy like code

A policy is a file in your repository, so you can test it in CI. moat policy check shows the verdict for a command against a given policy, without running it:

Terminal window
moat policy lint ./.moat/policy.yaml
moat policy check "npm run migrate:up" --policy ./.moat/policy.yaml --project .

That gives a reviewer a way to see what a policy change does before it merges.

See what the agents did

Rules are half of it. The other half is knowing what happened. Every decision goes into a local audit log that detects tampering:

Terminal window
moat report --since 7d # totals, hosts, top rules
moat replay --since today # a timeline per session

For a team, each person can export their log, and anyone can verify it without access to their machine:

Terminal window
moat audit export --since 7d > alice.jsonl
moat audit verify alice.jsonl
moat audit report alice.jsonl bob.jsonl

The export carries chain hashes, so an edited or deleted event shows up when it is verified. Credentials are redacted before they are stored, and nothing leaves a machine unless the person exports it.

Limits

  • Repository rules apply in the hook, not in the agents’ OS sandboxes. A repo deny stops the agent’s own calls. It does not stop a script that an allowed command starts.
  • Each developer runs OpenMoat on their own machine. There is no central server that pushes policy to everyone. A teammate who has not run moat init is not protected by the repository’s rules.
  • Agents differ. Codex cannot show an OpenMoat prompt, so an ask becomes a deny that the developer approves with moat allow --last. Cursor asks only for shell and MCP calls.
  • A deferred call runs if the agent lets it. In auto-approve modes, only deny and ask rules stop a call, which is a good reason to write the rules your team cares about as deny or ask.

The full list is in the threat model.

Try it

Terminal window
brew install crocodile-labs/tap/moat && moat init

Then add one deny rule your team already agrees on to .moat/policy.yaml, and commit it. The team rules guide has the details. If your team needs something this does not do, tell me on GitHub: features for teams are on the roadmap, and that feedback shapes them.