Team Workflows and Guardrails

Three engineers work on the Kitebase ticket service, the small Python codebase from earlier in this topic, and each has a coding agent set up their own way. Lena’s rules file says the tests run with python -m pytest -q. Priya got tired of permission prompts and allowed every shell command. Sam asked his agent for a --status flag; it asked to pip install click, he said yes, the tests passed on his laptop, and he opened a pull request saying so. It merged. The next morning the CLI fails on the support server with No module named 'click'.

Nobody did anything unreasonable. The team just had three setups and no shared floor. This last article in the topic builds that floor: the files, conventions and checks that make coding agents safe when several people point theirs at one repo.

What you’ll build: the Kitebase repo set up for a team: one rules file every tool reads, a committed .claude/settings.json, a pull request template, a GitHub Actions workflow that runs the tests and a secret scan on every pull request, and a small checker that confirms all of it is in place. None of it needs an API key.

Move the guardrails into the repo

When you use an agent alone, the guardrails live in your head. You know the test command, you’d never let it read .env, you read every diff. None of that reaches a teammate who clones the repo.

So a team moves them into files that arrive with git clone and change through pull requests like any code. Four layers do the work, and they differ in one way that matters: who enforces them.

One change, top to bottom In the Kitebase repo Who enforces it 1. RULES FILE AGENTS.md + CLAUDE.md Tests: python -m pytest -q No new runtime dependencies. Advice. Every agent reads it, and the model usually follows it. 2. PROJECT SETTINGS .claude/settings.json allow Bash(python -m pytest *) deny Read(./.env) Claude Code, on each laptop. Other tools don't read this file. 3. THE AUTHOR PR template [x] I read the whole diff myself AI: Claude Code wrote the filter People. The author owns every line, whoever typed it. 4. CI ON GITHUB required checks tests: ruff, python -m pytest -q secret-scan: gitleaks GitHub, for every PR, on a clean runner. Red can't merge.
The lower the layer, the less it depends on which tool someone uses or how careful they were that day.

The top layers help agents do the right thing. The bottom one catches it when they don’t, whatever tool wrote the change.

One rules file for every tool

A rules file is the Markdown file of standing instructions a coding agent reads at the start of every session; article 03 covers what goes in one. On a team, two things change.

First, teams rarely all use the same tool. AGENTS.md is the name most tools read, so the shared rules go there, and Claude Code’s CLAUDE.md imports it. Here’s Kitebase’s AGENTS.md:

# Kitebase tickets

A tiny ticket service. Python 3.10+, standard library only.

## Commands
- Tests: `python -m pytest -q`. CI runs this exact command on every pull request.
- Lint: `ruff check .`
- Try the CLI: `python -m tickets search sso`

## Rules
- Every new behavior gets a test. Don't change the assertions in existing tests; add new ones.
- A status must be one of `STATUSES` in `tickets/models.py`. Raise `ValueError` for anything else.
- Keep search functions pure: a list of tickets in, a list of tickets out.
- No new runtime dependencies. This runs on customers' servers where we can't pip install.
- Secrets live in `.env`, which is gitignored. Name the variable, never copy the value.

And the whole CLAUDE.md. The @AGENTS.md line is an import: Claude Code pastes that file in at launch.

@AGENTS.md

## Claude Code only
- Team permissions are in `.claude/settings.json`. Personal ones go in `.claude/settings.local.json`.
- Before you say a change is done, show the output of `python -m pytest -q` and `git diff --stat`.

Second, the file now records decisions the team made, not one person’s taste. Personal preferences go in your own ~/.claude/CLAUDE.md or a gitignored CLAUDE.local.md. Changes to the shared file go through a pull request like code. If your repo uses GitHub’s CODEOWNERS file (who must approve changes to which paths), list AGENTS.md, CLAUDE.md and .claude/ in it, so a rules change can’t hide inside an unrelated diff.

Note the test line: “CI runs this exact command”. When the agent says “tests pass”, it should mean what CI will mean. If the rules file says pytest -q and CI runs python -m pytest -q, the two can disagree, and the agent’s claim stops being evidence.

Project settings: one set of permissions

A rules file is advice. What must be enforced goes in settings, the JSON files Claude Code enforces whatever the model decides; article 03 covers how allow, ask and deny rules work. The team’s is .claude/settings.json, committed:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": ["Bash(python -m pytest *)", "Bash(ruff check *)", "Bash(python -m tickets *)"],
    "ask": ["Bash(git push *)", "Bash(pip install *)"],
    "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
  }
}

Allow what every teammate would approve every time: the tests, the linter, the CLI. Ask before anything that changes things outside your working copy. Deny the secrets. pip install is an ask, which is what happened to Sam: the agent asked and he said yes. The settings did their job; the lower layers catch the rest.

What makes this a team file, from Claude Code’s settings and permissions docs:

  • Deny beats allow across every file. Claude Code merges user, project and local settings and checks deny first, so Priya’s personal Bash(*) in .claude/settings.local.json can’t undo the team’s deny on .env.
  • Personal exceptions stay out of git. Claude Code adds .claude/settings.local.json to your global git ignore when it creates it. Kitebase’s .gitignore lists it anyway, with .env and CLAUDE.local.md.
  • Allow rules wait for trust. They apply once each person accepts the trust prompt for the repo. Deny and ask rules apply straight away.
  • It only binds Claude Code. A teammate on Cursor never reads it. Their tool has its own settings, and CI covers everyone.

A policy nobody may override belongs in managed settings, a file an admin deploys to every machine, not in a repo.

AI-assisted pull requests: the author owns them

A pull request (PR) is where a change stops being yours alone. The conventions that hold up are the ones you’d want for any PR, written down because agents make them easy to skip:

  1. The person who opens the PR owns every line. “The agent wrote that” isn’t an answer in review. If you can’t explain a line, you’re not done.
  2. Review your own diff first. git diff --stat, the whole diff, the tests, and one run by hand. Article 06 covers what to look for.
  3. Say how you checked it. The test output line and the command you ran, so the reviewer spends time on design, not on rerunning your checks.
  4. Say what was AI-assisted, if your team wants that. Not as a confession: it tells the reviewer where to look harder. Tests an agent wrote with its own code can check what the code does rather than what it should do.
  5. Keep it reviewable. A 40-file diff gets skimmed. Article 07 shows how to split one.

A PR template puts the checklist in front of every author. On GitHub, a file at .github/pull_request_template.md pre-fills the description of every new pull request. Kitebase’s:

## What and why

<!-- One or two sentences. What changes for a user of the CLI? -->

## How I checked it

- [ ] I read the whole diff myself before asking for review.
- [ ] `python -m pytest -q` passes. Last line of the output:
- [ ] I ran it by hand. Command and what it printed:

## AI assistance

<!-- Which tool, and which parts. "None" is a fine answer.
     e.g. "Claude Code wrote the --status filter and its tests. I rewrote the error message." -->

Claude Code already marks its own work unless you tell it not to: commits it writes get a Co-Authored-By trailer naming the model, and PR descriptions it writes get a “Generated with Claude Code” line. The attribution key in settings changes or hides both, so a team that wants one convention can set it once in .claude/settings.json.

Should we require people to disclose AI use at all?

Decide as a team and write it down; either answer works if it’s consistent. For: reviewers know where to look harder. Against a strict rule: nearly every change involves some autocomplete, and a box everyone ticks tells nobody anything. The template above is a middle ground: one sentence on which parts, when an agent wrote a meaningful chunk.

CI: the same checks for every pull request

CI (continuous integration) means automated checks that run on a fresh machine for every pull request. It’s the only layer that doesn’t depend on which tool someone used or how careful they were.

Sam’s PR is the case CI exists for. His laptop had click installed. The repo didn’t list it, so a clean machine doesn’t have it, and with the workflow below the same PR goes red:

SAM'S LAPTOP 1. INSTALL pip install click the agent asks, Sam says yes 2. TEST python -m pytest -q 4 passed 3. OPEN THE PR "--status filter, tests pass" on: pull_request CI RUNNER, CLEAN 1. INSTALL pip install -r requirements-dev.txt 2. TEST ModuleNotFoundError: No module named 'click' 3. RESULT tests job failed, merge blocked Only what's committed: pytest and ruff. click was never in the repo. Red before any reviewer opened the diff.
Same command, same code, different machine. The error is what pytest prints on a clean install.

A GitHub Actions workflow is a YAML file in .github/workflows/ that says which checks to run and when. Kitebase’s runs two jobs:

name: ci
on:
  pull_request:
  push:
    branches: [main]
permissions:
  contents: read

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - run: pip install -r requirements-dev.txt
      - run: ruff check .
      - run: python -m pytest -q

  secret-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write # gitleaks comments on the lines it flags
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0 # scan every commit in the PR, not just the last one
      - uses: gitleaks/gitleaks-action@v3
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # only needed for organization repos
  • on: pull_request runs it on every PR, whoever wrote it. A workflow that only runs on pushes to main checks code after it merged.
  • The test job installs only what’s committed (pytest and ruff) and runs the command the rules file names. That’s how click gets caught.
  • The secret scan uses gitleaks, an open-source scanner that looks for strings shaped like keys and tokens. fetch-depth: 0 fetches the whole history, so a secret added in one commit and deleted in the next is still found. Organization repos need a free gitleaks license key, stored as a secret.
  • permissions keeps the workflow’s token read-only, plus the one write gitleaks needs to comment.

A red check only blocks a merge if you make it a required status check in the branch’s branch protection rules (GitHub’s settings for what must pass before anything merges into main). Do that for both jobs. By default admins can still bypass the rules; GitHub has a setting to apply them to admins too.

Two gotchas. A secret CI finds has already been pushed, so treat it as leaked: rotate it (issue a new one, revoke the old). And @v6 is a tag the action’s owner can move; GitHub’s security guidance says pinning to a full commit SHA is the only way to make an action version immutable.

Check the setup itself

The setup is files, and files drift: a trailing comma in settings.json, a test command changed in CI but not in AGENTS.md. The companion’s main.py checks a repo for the whole setup. Its test-command check compares CI with the rules file:

def check_ci_tests(workflows, documented):
    runs = [(name, s["run"]) for name, wf in workflows.items() for s in steps(wf) if "run" in s]
    if documented:
        doc_file, command = documented
        for name, run in runs:
            if any(line.strip() == command or line.strip().startswith(command + " ")
                   for line in run.splitlines()):
                return Result("ok", "ci-tests", f"{name} runs `{command}`, the command in {doc_file}")
    ...

Run on the Kitebase repo, and on the “solo” setup the team started from:

kitebase
  ok    rules-file     AGENTS.md gives the test command `python -m pytest -q`
  ok    settings-json  .claude/settings.json is valid JSON
  ok    deny-env       deny rules cover .env and .env.*
  ok    allow          3 allow rules, each for one command
  ok    ci-on-pr       ci.yml runs on pull_request
  ok    ci-tests       ci.yml runs `python -m pytest -q`, the command in AGENTS.md
  ok    secret-scan    ci.yml runs gitleaks/gitleaks-action@v3
  ok    pr-template    .github/pull_request_template.md asks how it was checked and about AI
  ok    gitignore      .gitignore keeps .env and personal Claude files out of git
  ready for the team

solo-setup
  WARN  rules-file     CLAUDE.md only. Put shared rules in AGENTS.md, which other tools read too.
  ok    settings-json  .claude/settings.json is valid JSON
  FAIL  deny-env       nothing denies Read(./.env). Add it to permissions.deny.
  FAIL  allow          Bash(*) lets the agent run any command without asking.
  FAIL  ci-on-pr       no workflow runs on pull_request, so PRs merge unchecked.
  WARN  ci-tests       ci.yml runs `pytest` but CLAUDE.md says `python -m pytest -q`. ...
  FAIL  secret-scan    no secret scan in CI. Add gitleaks or trufflehog.
  WARN  pr-template    no PR template. Reviewers can't tell how a change was checked.
  WARN  gitignore      .gitignore is missing .env, CLAUDE.local.md, .claude/settings.local.json
  4 failed, 4 warnings

The JSON check matters more than it looks. Settings files are strict JSON, so one trailing comma breaks the file. An interactive session shows a Settings Error; claude --help says that in non-interactive mode “settings files that fail validation are silently ignored”, deny rules and all. The checker exits with 1 on a failure, so it can run in CI too.

Agents in CI: a helper, not a gate

With the floor in place, you can run an agent inside CI too, to review a diff or answer an @claude comment. Stick to what the tools document. Claude Code documents two ways.

Headless mode. claude -p (--print) runs one prompt without the interactive screen and exits, so a script can call it. A review step might look like this; every flag is in claude --help or the headless docs:

gh pr diff "$PR_NUMBER" | claude --bare -p \
  "Review this diff against the rules in AGENTS.md. List problems only, with file and line." \
  --append-system-prompt-file AGENTS.md \
  --allowedTools "Read" --permission-mode dontAsk \
  --max-budget-usd 1 --output-format json

--allowedTools "Read" pre-approves file reads only. --permission-mode dontAsk denies anything that would have asked, since nobody is there to answer. --max-budget-usd caps the spend. --bare skips the repo’s hooks, MCP servers and CLAUDE.md (hence passing AGENTS.md by hand) and needs an ANTHROPIC_API_KEY.

The GitHub Action. anthropics/claude-code-action@v1 runs Claude Code in a workflow when someone mentions @claude in a PR or issue, or on any event with a prompt you give it. Only users with write access can trigger it, and on public repos GitHub withholds secrets from pull requests opened from forks.

Two rules for either way:

  1. Its output is a comment, not a check. A model’s review can miss things and can differ between runs on the same diff. A required check must give the same answer every time, so the gates stay tests, lint and the scan, and a human approves the merge.
  2. Never let the PR decide what the agent runs. This is the gotcha:
A pull request's branch contains claude -p on that checkout, no trust dialog .claude/settings.json: "hooks" Run, as commands on your runner .mcp.json: a new server Connected without asking .claude/settings.json with a stray comma Skipped silently, deny rules too .claude/settings.json: "allow" Not used; a warning on stderr SAFER IN CI claude --bare -p skips the repo's hooks, .mcp.json and CLAUDE.md anthropics/claude-code-action@v1 restores .claude/, .mcp.json and CLAUDE.md from the base branch
From Claude Code's permissions docs, 'What runs before you trust a folder'.

A claude -p run never shows the trust prompt, yet it still uses the project’s hooks, its env block and the servers in .mcp.json. So if CI runs claude -p in a PR’s checkout, whoever wrote the PR chose commands that run next to your secrets. Use --bare (or --setting-sources user), or the action, which restores .claude/, .mcp.json and CLAUDE.md from the base branch before Claude starts. Its security docs add: don’t check out an untrusted branch into the workspace root before it.

Roll it out in steps

Don’t switch the whole team over at once. Each step is useful on its own and makes the next one safer:

  1. Put the floor in first. CI with tests and a secret scan on every PR, as required checks. It’s worth having even if nobody uses AI.
  2. Settle where code may go. Which tool and plan, and whether the vendor may train on your code; article 02 has the questions.
  3. Commit the shared files from one working setup, as a PR the whole team reviews.
  4. Start with two or three volunteers on real tickets. Same mistake twice: add a rule. Same prompt approved every time: add an allow rule.
  5. Add the PR template, then open it up to everyone.
  6. Agents in CI last, as comments only, with a budget cap.

Someone will ask whether it’s working. Lines of code and PR counts go up with these tools and say nothing about whether you ship better software. Track time from PR opened to merged, how often CI goes red and why, and how often you revert. The first is part of what DORA’s delivery research calls change lead time, and reverts are a rough stand-in for its change fail rate. And ask reviewers whether reviewing got harder: if review time climbs while authoring speeds up, the cost moved, it didn’t go away.

Try it yourself

The companion has the Kitebase repo set up for a team, the solo setup it replaced, and the checker. Nothing calls a model.

Download the runnable example (zip)

cd 09-team-workflows
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python main.py

Then try these:

  1. Add a trailing comma after the last deny rule in kitebase/.claude/settings.json and run python main.py kitebase. The settings check fails. Then remove it.
  2. Change the test command in kitebase/AGENTS.md to pytest -q. The checker warns that CI and the rules file disagree, and the companion’s own pytest -q fails two tests. Change it back.
  3. Put kitebase/ in a new GitHub repo and open a pull request. Both jobs run. In the branch protection settings, make tests and secret-scan required, then push a commit that imports click and watch GitHub block the merge.

pytest -q runs the offline tests, including one that copies the Kitebase repo and runs its tests with the command CI uses.

Common beginner mistakes

  • Everyone keeps their own rules file. Each agent learns different conventions. Commit one AGENTS.md.
  • Treating a green laptop as a green build. Your machine has packages, files and environment variables the repo doesn’t. Only CI starts clean.
  • CI that isn’t required. A red check nobody has to wait for is a suggestion. Make the jobs required status checks.
  • “The agent wrote it” in review. The author owns the PR. Read your own diff before anyone else has to.
  • Running claude -p on an untrusted checkout. The PR’s hooks and MCP servers run. Use --bare or the action.

Questions you will face in production

“A teammate uses Cursor. Are they covered?” By AGENTS.md, yes, because Cursor reads it. By .claude/settings.json, no. Set up the matching permissions in their tool, and rely on CI, which checks their PRs the same as everyone’s.

“Should we just ban AI tools until there’s a policy?” Usually no: people use them anyway, invisibly. A short written policy works better: which tool and plan, the shared files, the PR conventions, and CI as the floor.

“Can we let the agent merge its own PRs if CI is green?” Not as a default. CI proves the change passes the checks you wrote, not that it’s the right change. Keep a human approval on every PR. Claude’s GitHub Action is built the same way: by default it links to GitHub’s PR page and lets a person open the PR.

Check your understanding

Priya adds "Bash(*)" to her .claude/settings.local.json. Can her agent now read the Kitebase .env file?

Not with Claude’s file tools or commands like cat .env: the team’s Read(./.env) deny is checked before any allow. It isn’t a hard boundary, though. A script the agent writes can still open the file, so a secret that must never leak needs Claude Code’s sandbox, or no .env on that machine.

Sam's agent says "all tests pass" and CI goes red on the same commit. Name two likely causes.

The laptop has something the repo doesn’t: an installed package like click, an untracked file, or an environment variable. Or the two ran different commands, such as pytest locally and python -m pytest -q in CI. The fix for the second is one command, written in AGENTS.md and used in the workflow.

Your CI runs claude -p on every PR's checkout to post a review. What could a malicious PR do, and what do you change?

Add a hook to .claude/settings.json or a server to .mcp.json, which claude -p would run on your runner, next to your API key. Use --bare or --setting-sources user, or anthropics/claude-code-action@v1, which restores that config from the base branch.

Gitleaks flags a token in a PR. The author deletes the line and pushes again. Is that enough?

No. The token is in the history and was pushed to GitHub, so it’s leaked: revoke it and issue a new one. With fetch-depth: 0 the scan keeps flagging the old commit, which is the point. Clean the history after rotating, not instead.

What to remember

  • On a team, guardrails move out of people’s heads into the repo: a rules file, project settings, a PR template and CI.
  • Put shared rules in AGENTS.md and import it from CLAUDE.md. Name the exact test command CI runs.
  • .claude/settings.json gives everyone the same allow, ask and deny rules; a deny anywhere beats an allow anywhere. It only binds Claude Code.
  • The author owns an AI-assisted PR: they review it first, say how they checked it, and say what was AI-assisted where the team asks.
  • CI with tests and a secret scan, as required checks, is the one layer that treats every change the same.
  • Agents in CI post comments; they don’t gate merges, and they never run config from an untrusted PR.

What to study next

That’s the end of the AI Coding Tools topic, and the end of the curriculum. Every tool in it is the same machine underneath: a model in a loop, choosing tools, reading results and deciding when it’s done. If you want to build that loop yourself instead of configuring someone else’s, start with what an AI agent actually is. If you’d rather give your team’s agents new tools of their own, MCP and Custom Tools in Your Editor is the place to go back to.

Further reading

Where this article comes from. This is a synthesis of common practice in AI engineering as of 2026, not a citation of any single paper. The sources above are where the mechanics come from; the Claude Code details were checked against version 2.1.281 and its docs. If you find an error or have a better source for a claim, the article gets fixed within a day, send me a note.


Auto-marks when you reach the end. Click to toggle.