Skip to content

Repository files navigation

agent-policy

The enforced companion to AGENTS.md.

One YAML file declaring what your AI coding agent may not do, compiled to the native permission config of whichever runtime you use. An attempt at a specification for standardizing agentic permission enforcement, treating enforcement as a layer separate from the written context standardized by AGENTS.md.

Status: Version: 0.1.0-draft. Nothing is stable. See open questions.


Why

AGENTS.md is the settled standard for agent context. However, it lacks an enforcement layer.

The emerging convention is three tiers — always do, ask first, never do — written like this:

## Boundaries
- Never commit secrets
- Don't touch infra/prod/
- Never force-push to main

Those are three sentences in a context window. The model usually honors them. "Usually" is not a security control.

Meanwhile every major runtime already has a real permission system that would enforce exactly these rules, and each uses a different format:

Runtime Config
Claude Code .claude/settings.json
Codex .codex/config.toml
OpenCode opencode.json
Cursor .cursor/rules/, hooks.json

So the tier everyone writes in prose is unenforced, and the enforcement that exists is fragmented four ways. agent-policy is the compile step between them.

How it fits

AGENTS.md                    prose, into the model's context — how to work
agent-policy.yaml            source of truth — what is forbidden
     │
     └── compile ──┬──> .claude/settings.json    native enforcement
                   ├──> .codex/config.toml       native enforcement
                   ├──> opencode.json            native enforcement
                   └──> .agent-policy/hook.py    fallback, remainder only

The split is by whether a rule has a deterministic trigger:

  • "Use conventional commits" → no trigger → AGENTS.md
  • "Never force-push to main" → fires on a specific argv → agent-policy.yaml

Both are git workflow. Different files.

Keep a Boundaries section in AGENTS.md, shortened to a summary that points here — not for redundancy, but so the agent knows the shape of its constraints instead of discovering them by getting blocked. The prose informs and the policy enforces. See the example AGENTS.md in this repo for what that looks like.

Enforcement priority

  1. Native runtime config (compiled). Always preferred — battle-tested, and applied before the agent takes its first turn.
  2. The bundled hook. For runtimes with no native permission system, or for residual rules no native target could express.
  3. OS sandbox. Underneath both. A Read deny rule blocks the built-in file tool, not a Python script that opens the file itself. Guardrail, not containment.

What is in this repo

This is the spec and reference implementation. You read it here and install pieces into your own projects.

├── SPEC.md                       # normative: precedence, categories, target mappings
├── agent-policy.schema.json      # JSON Schema (draft 2020-12)
├── agent-policy.yaml             # blank template — copy to your repo and fill in
├── AGENTS.md                     # blank template — the advisory half of the pair
├── compile/
│   ├── export_claude_code.py     # policy -> .claude/settings.json
│   ├── import_claude_code.py     # existing settings.json -> policy (onboarding)
│   ├── export_codex.py           # policy -> .codex/config.toml (permission profile)
│   ├── import_codex.py           # existing config.toml -> policy (onboarding)
│   ├── export_opencode.py        # policy -> opencode.json (per-tool permissions)
│   └── import_opencode.py        # existing opencode.json -> policy (onboarding)
├── enforcers/
│   └── pretooluse_hook.py        # fallback enforcer
├── tools/
│   ├── validate.py               # CI validator + merge implementation
│   └── sync_agents_md.py         # render/check the AGENTS.md Boundaries summary
├── tests/
│   └── test_agent_policy.py      # conformance fixtures (stdlib unittest)
└── examples/
    └── python-fastapi/           # filled worked example: agent-policy.yaml + paired AGENTS.md

The root agent-policy.yaml and AGENTS.md are blank templates — every field present, nothing filled in — meant to be copied into your repo and completed. examples/ holds the same pair filled in for a real (fictional) project, as a reference.

Quickstart

Already have Claude Code permission rules? Import them once:

python3 compile/import_claude_code.py .claude/settings.json > agent-policy.yaml

Starting fresh? Copy the blank agent-policy.yaml and AGENTS.md templates (plus agent-policy.schema.json) to your repo root and fill them in — every field is present, empty. A filled reference is in examples/python-fastapi/. A short enforced policy beats a long one that gets switched off.

Then compile:

python3 compile/export_claude_code.py --report    # see coverage first
python3 compile/export_claude_code.py             # write .claude/settings.json

The report tells you what compiled natively and what fell through to the hook:

claude-code: 47 permission rules, 5 allowed + 0 denied domains
  2 rule(s) not expressible natively -> hook target:
    - escalation: no native equivalent (approver/timeout)
    - audit: no native equivalent (structured decision log)

On Codex? Same policy, different target:

python3 compile/export_codex.py --report    # see coverage first
python3 compile/export_codex.py             # write .codex/config.toml
python3 compile/import_codex.py .codex/config.toml > agent-policy.yaml   # or onboard

Codex ≥ 0.119 has a fine-grained permission profile — per-path read/write/deny and per-domain network allow/deny — so export_codex.py compiles a [permissions.agent-policy] profile that enforces read/write/network natively (only exec falls to the hook, since Codex keeps command policy in a separate execpolicy mechanism). For older Codex, set targets.codex.style: sandbox for the coarse sandbox_mode posture. See SPEC.md.

On OpenCode? Same again:

python3 compile/export_opencode.py --report    # see coverage first
python3 compile/export_opencode.py             # write opencode.json
python3 compile/import_opencode.py opencode.json > agent-policy.yaml   # or onboard

OpenCode's permission block is per-tool (read/edit/bash/… → {pattern: action}), so read/write/exec compile natively. Only network falls to the hook, because webfetch/websearch are whole tool toggles with no per-domain form. See SPEC.md.

Sync your AGENTS.md. Drop two markers into your AGENTS.md and let the policy fill the Boundaries summary between them:

python3 tools/sync_agents_md.py --write    # render the block from the policy
python3 tools/sync_agents_md.py            # check for drift (CI gate)

Everything outside the markers stays yours; the block between them is generated, so the summary can never quietly fall out of step with what is enforced.

Add CI:

# .github/workflows/agent-policy.yml
name: agent-policy
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install pyyaml jsonschema
      - run: python3 tools/validate.py
      - run: python3 tools/sync_agents_md.py    # fail if AGENTS.md drifted from the policy
      - run: python3 tests/test_agent_policy.py
      - run: |
          python3 compile/export_claude_code.py
          python3 compile/export_codex.py
          python3 compile/export_opencode.py
          git diff --exit-code .claude/settings.json .codex/config.toml opencode.json

The last step fails the build if any compiled output is stale, ensuring that the policy and the config it generates never silently diverge.


Policy format

Precedence is deny → ask → allow, first match wins, deny beats allow.

Categories are runtime-neutral. Tool names differ across runtimes (Bash vs shell); capabilities do not. The compiler owns the mapping.

version: "0.1.0-draft"
mode: warn

permissions:
  deny:
    read: ["**/.env", "secrets/**"]
    write: ["infra/prod/**", ".github/workflows/**"]
    exec: ["git push --force", "terraform apply", "curl"]
  ask:
    exec: ["git push", "alembic upgrade"]
  allow:
    write: ["src/**", "tests/**", "docs/**"]
    network: ["github.com", "pypi.org"]

targets:
  claude-code:
    output: .claude/settings.json
    merge: true
  hook:
    output: .agent-policy/hook.py
    covers: remainder

Write bare command prefixes - the compiler adds each target's wildcard syntax. The full category and precedence semantics, and every per-target mapping, are in SPEC.md.

Mode

mode (audit / warn / enforce) applies to the hook only — native targets enforce unconditionally once compiled. Start at warn to learn your false-positive rate before promoting to enforce. Full behavior table in SPEC.md.

Nested policies

One agent-policy.yaml per directory, merged root-down — denials union, allowances intersect, mode from the root, so a subdirectory can tighten a policy but never weaken it (the inverse of AGENTS.md's nearest-file-wins). tools/validate.py --effective <dir> prints the resolved policy. Normative rules in SPEC.md.


Non-goals

  • Replacing AGENTS.md. Adopt it. This handles one section of it.
  • A new enforcement engine. The runtimes already have those. This targets them.
  • Agent topology. How agents and tools wire together is a separate problem, addressed by things like Oracle's Agent Spec.
  • Roles or personas. No opinion on what a "Debugger agent" is.
  • A sandbox. Policy expression, not isolation. Use a container for containment.

Known weakness

Compiled output lands where the runtime reads it before the agent's first turn, so the agent cannot edit its own permissions mid-session. Better than a policy file it can write to. Still not a guarantee as an agent with repo write access can edit agent-policy.yaml and recompile.

The honest fix is enforcing from outside the workspace. v0.1.0-draft does not solve this and does not attempt to.

Roadmap

v0.1.0-draft covers claude-code, codex, and opencode (each with export + import), a fallback hook with a documented contract, nested-policy resolution, and AGENTS.md drift detection. What is intended next, roughly in order:

More targets

  • Cursor. Compile to .cursor/rules/ (MDC) and hooks.json. Its model is prose rules plus lifecycle hooks rather than a permission table, so the split between what maps natively and what falls to the hook will differ again — the next update.
  • Codex execpolicy. exec rules are the one thing Codex config.toml cannot express; generating a Codex execpolicy would move per-command rules from the hook to native.
  • Claude WebFetch(domain:…). Extend network enforcement from sandboxed shell egress to the WebFetch tool via permission rules (today the hook covers the tool path).

Solidifying the hook

  • Freeze per-runtime adapter wire formats against each runtime's shipped hook contract (Claude Code PreToolUse, Codex [hooks]), with fixtures per runtime.
  • Package the hook so installation is a single documented step rather than a copied file, and let it resolve nested policy the same way validate.py does without duplication.

Harder problems (tracked in SPEC.md)

  • Tamper resistance. Enforcement from outside the workspace — the only real answer to an agent that can edit agent-policy.yaml and recompile.
  • Per-agent scoping and an allowlist mode for exec

About

agent-policy - Specify agentic permissions enforcement

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages