Skip to content

Example rule files

Generated from agentltl-coding/examples/

It is kept with the code it documents. Edit it there.

showcase.yaml

Showcase rules: each one does something CLAUDE.md or permission rules cannot.

# Showcase rules: each one does something CLAUDE.md or permission rules cannot.
# Every rule here is exercised by `agentltl check` (see README, Examples).

rules:
  - id: staging-before-prod
    before:
      first: {tool: make, with: {argv: deploy-staging}}
      then: {tool: make, with: {argv: deploy-prod}}
      since: [Edit, Write]
    scope: project
    why: Prod only gets what staging has already run.

  - id: plan-before-apply
    before: {first: terraform_plan, then: terraform_apply, since: [Edit, Write]}
    why: Never apply infrastructure changes nobody has seen planned.

  - id: one-pr-per-session
    at_most: {call: gh_pr_create, times: 1}
    mode: ask
    why: A retried `gh pr create` opens duplicate PRs.

  - id: no-push-to-main
    never: git_push
    where: {refspecs: [main, "*:main"]}
    why: Main only changes through reviewed PRs.

  - id: destructive-git
    never:
      - {tool: git_reset, with: {hard: true}}
      - {tool: git_clean, with: {force: true}}
      - {tool: git_checkout, with: {targets: "."}}
      - {tool: git_branch, with: {force_delete: true}}
      - {tool: docker_compose_down, with: {volumes: true}}
      - docker_system_prune
    mode: warn
    why: These throw away work or data that cannot be recovered.

  - id: stay-in-scope
    require: {tool: [Edit, Write], where: {file_path: ["src/**", "tests/**"]}}
    mode: ask
    why: The task is limited to src/ and tests/.

  - id: no-prod-data
    never:
      - {tool: [psql, mysql, redis_cli], where: {argv: "*prod*"}}
      - {tool: "kubectl_*", where: {"*": "*prod*"}}     # namespace, context, names...
    mode: stop
    why: Production data is off limits to the agent.

  - id: known-hosts-only
    require:
      - {tool: curl, where: {urls: ["https://api.github.com/*", "https://pypi.org/*"]}}
      - {tool: WebFetch, where: {url: ["https://docs.python.org/*", "https://pypi.org/*"]}}
    mode: ask
    why: Network access is limited to known hosts.

  - id: lint-before-commit
    before: {first: ruff, then: git_commit, since: [Edit, Write]}
    mode: retry
    why: Commits must pass ruff.

  - id: audit-sudo
    never: sudo
    mode: log
    why: Every sudo is worth a second look.

creative.yaml

Creative rules: workflows, research hygiene and security tripwires.

# Creative rules: workflows, research hygiene and security tripwires.
# Each one is exercised with `agentltl check` in the commit that added it.

rules:
  # Numbers in results/ must come out of code, never typed in by the model.
  - id: no-hand-typed-results
    never: {tool: [Edit, Write, NotebookEdit], where: {file_path: ["results/**", "figures/**"]}}
    why: Results are produced by scripts, so every number can be traced to code.
    fix: Change the script that produces the file and run it.

  # Every experiment run maps to a commit.
  - id: commit-before-experiment
    before:
      first: git_commit
      then: {tool: python, where: {script: "train*.py"}}
      since: [Edit, Write]
    why: An experiment run on uncommitted code cannot be reproduced.

  # Plan before code.
  - id: plan-first
    before:
      first: {tool: Write, where: {file_path: PLAN.md}}
      then: {tool: [Edit, Write], where: {file_path: "src/**"}}
    why: Write the plan to PLAN.md before touching src/.

  # Test-first: the test changes before the code does.
  - id: test-first
    before:
      first: {tool: [Edit, Write], where: {file_path: "tests/**"}}
      then: {tool: [Edit, Write], where: {file_path: "src/**"}}
      since: git_commit
    mode: warn
    why: Write or change a test before the implementation, in every commit.

  # Prompt-injection tripwire: nothing legitimate ever reads the canary.
  - id: canary
    never: {tool: [Read, cat, head, tail, grep, cp, Grep], where: {"*": "**/CANARY_*"}}
    mode: stop
    why: The canary file is bait; reading it means instructions came from untrusted content.

  # Break the exfiltration chain: after reading the web, outbound sends need you.
  - id: web-then-send
    ltl: 'G(now("WebFetch") -> G(!now("curl") & !now("wget") & !now("scp")))'
    mode: ask
    why: Untrusted web content was read this session; sending data out needs your approval.

  # Conventional commits.
  - id: conventional-commits
    require: {tool: git_commit, where: {message: ["feat*", "fix*", "docs*", "test*", "refactor*", "chore*"]}}
    mode: retry
    why: Commit messages follow Conventional Commits (feat:, fix:, docs:, ...).

  # Subagent budget.
  - id: subagent-budget
    at_most: {call: [Agent, Task], times: 3}
    mode: ask
    why: Subagents are expensive; ask before a fourth.

  # Look before you overwrite: the same file must have been read first.
  - id: read-before-overwrite
    before:
      first: {tool: Read, with: {file_path: $f}}
      then:
        - {tool: [Edit, Write], with: {file_path: $f}, exists: true}
        - {tool: [sed, perl, tee], with: {paths: $f}, exists: true}   # in-place edits, tee
        - {tool: sponge, with: {file: $f}, exists: true}
        - {tool: "*", with: {overwrite_to: $f}, exists: true}         # > file (>> only appends)
    why: Read a file before replacing it.

  # Decision log before anything destructive.
  - id: log-before-destroy
    before:
      first: {tool: [Edit, Write], where: {file_path: DECISIONS.md}}
      then: [{tool: rm, with: {recursive: true}}, {tool: git_reset, with: {hard: true}}]
    why: Record why in DECISIONS.md before deleting anything.

  # A runbook that enforces itself.
  - id: drain-before-reboot
    before:
      first: kubectl_drain
      then: {tool: ssh, where: {command: "*reboot*"}}
    scope: project
    why: Drain the node before rebooting it.

AGENTLTL.yaml

AGENTLTL.yaml – rules the AgentLTL plugin enforces on every Claude Code tool call.

# AGENTLTL.yaml – rules the AgentLTL plugin enforces on every Claude Code tool call.
# Shell commands are parsed into structured calls first; list the names with `agentltl tools`.

settings:
  mode: block            # default escalation: block | warn | retry | ask | stop | log
  retries: 3             # for mode: retry, blocked attempts before you are asked
  unparseable:           # a shell command the guard cannot analyse
    interactive: ask     # normal permission modes: you decide (ask | note | allow | deny)
    auto: note           # auto mode: let it through, tell Claude it was not checked
  announce: true         # list the rules to Claude at session start and after compaction

rules:
  - id: tests-before-push
    before: {first: [pytest, {tool: make, with: {argv: test}}], then: git_push, since: [Edit, Write]}
    why: CI takes 20 minutes; a red build blocks everyone.
    fix: Run the test suite (pytest) after your last edit, then push.

  - id: no-force-push
    never: git_push
    with: {force: true}
    why: Force-pushing rewrites shared history.
    mode: warn

  - id: no-secrets
    never: [Edit, Write, Read, cat, cp, mv, rm, echo, tee]
    where: {"*": ["**/.env", "**/.env.*"]}
    why: .env files hold credentials.
    mode: stop

  - id: lockfile-by-tool-only
    never: [Edit, Write]
    where: {file_path: "*.lock"}
    why: Lockfiles are generated; edit the manifest and re-run the package manager.
    mode: ask

  - id: one-migration
    at_most: {call: {tool: alembic_revision}, times: 1}
    why: One migration per change.
    mode: retry

tools:                   # cli-to-tools specs for project commands
  alembic:
    subcommands:
      revision:
        options:
          - {flags: [-m, --message]}
          - {flags: [--autogenerate], type: bool}
      upgrade: {positionals: [{name: revision}]}