One Claude for the Whole Team: keeping agent config in its own repository
Every team that starts using an AI coding agent seriously runs into the same wall at about the same time. Not “the model is not smart enough”. Something much more boring: nobody can agree on what the agent is supposed to know.
We run four independently deployable services, each in its own repository: a React frontend monorepo, a .NET backend, a Go document-processing service, and a set of Lambdas. An infrastructure repo sits next to them. For about a year we did the obvious thing and dropped a CLAUDE.md into every repo root. Then we stopped doing that, moved the whole configuration into a repository of its own, and cloned it as .claude/ at the workspace root.
This is what that setup looks like, why we ended up there, and what it actually costs.

The part that quietly falls apart
For a single repository, a CLAUDE.md in the root works fine. It is the recommended thing, and it is genuinely good advice. The trouble starts the moment you have a second service.
The same rule now lives in four files. “Merge requests target develop, commits follow Conventional Commits.” Four copies. Three of them are already out of date, and nobody can tell you which three without opening all four.
Everyone quietly builds their own agent. Developers keep their skills and prompts in a personal ~/.claude/. Two people run the same task and get different results, and the difference is impossible to argue about, because it is not in any repository. It is on somebody’s laptop.
A session sees one repo. A task does not. “Add a field to the form” means a migration and a DTO in the backend, a regenerated OpenAPI contract, a component and validation rules in the frontend, and an updated E2E test. Split per service, that is three sessions, three re-explanations of the same context, and three decisions that do not fully agree with each other.
Feedback never accumulates. A reviewer leaves a comment about a convention that is not written down anywhere. Two weeks later, the same comment shows up on a different merge request. The knowledge lives in people’s heads, and heads do not merge.

Lift the config one level up
The fix turned out to be almost embarrassingly simple. Do not put the config in the repositories. Put it one level above them, at the workspace root where all repos are siblings, and make the config itself a separate git repository, cloned as .claude/.
Sessions then open from the workspace root, never from inside a service. The agent sees every service at once. The rules are shared, versioned, reviewed like code, and delivered to everyone automatically.
~/workspace/
├── .claude/ <- a git repository of its own
│ ├── CLAUDE.md
│ ├── .mcp.json
│ ├── settings.json
│ ├── rules/ policy, always loaded
│ ├── skills/ procedures, loaded on invocation
│ ├── commands/
│ ├── agents/
│ ├── hooks/
│ ├── templates/
│ └── scripts/
├── CLAUDE.md -> symlink to .claude/CLAUDE.md
├── .mcp.json -> symlink to .claude/.mcp.json
├── .mise.toml -> symlink to .claude/.mise.toml
├── .work/ <- per-task context, never committed
├── web/ repo 1, frontend monorepo
├── api/ repo 2, backend
├── microservice/ repo 3, document processing
└── infra/ repo 4, infrastructure
The whole trick is in those three symlinks. Claude Code reads CLAUDE.md and .mcp.json from the session root, but we want them versioned inside the config repo. A symlink satisfies both at once: one source of truth, git sees the file where it belongs, the agent sees it where it expects it.
Bootstrap is two commands
mkdir -p ~/workspace && cd ~/workspace
git clone git@gitlab.com:acme/web.git
git clone git@gitlab.com:acme/api.git
git clone git@gitlab.com:acme/microservice.git
git clone git@gitlab.com:acme/infra.git
git clone git@gitlab.com:acme/claude.git .claude # note: as .claude
.claude/scripts/bootstrap.sh
bootstrap.sh is deliberately boring. It figures out the workspace root from its own location and lays down the symlinks:
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
CLAUDE_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd -P)"
WORKSPACE_ROOT="$(cd -- "$CLAUDE_DIR/.." && pwd -P)"
# refuse to run if the repo was cloned as anything but .claude/
if [[ "$(basename "$CLAUDE_DIR")" != ".claude" ]]; then
echo "[bootstrap] ERROR: this repo must live at <workspace>/.claude/" >&2
exit 1
fi
link() {
local src="$1" dst="$2"
# never clobber a real file that is already there
if [[ -e "$dst" && ! -L "$dst" ]]; then
echo "[bootstrap] WARNING: $dst exists and is not a symlink, skipping" >&2
return
fi
ln -sfn "$src" "$dst"
}
link "${CLAUDE_DIR}/CLAUDE.md" "${WORKSPACE_ROOT}/CLAUDE.md"
link "${CLAUDE_DIR}/.mcp.json" "${WORKSPACE_ROOT}/.mcp.json"
link "${CLAUDE_DIR}/.mise.toml" "${WORKSPACE_ROOT}/.mise.toml"
Two things trip up literally everyone on their first day, so we say them out loud during onboarding:
- Clone it as
.claude, not asclaude. Otherwise nothing loads at all. - Open the session from the workspace root, not from inside a service. Otherwise no rules and no skills load, and the symptom looks exactly like “the agent is being dumb today”.
What lives inside the config repo
| Path | What it does |
|---|---|
CLAUDE.md |
Workspace-level instructions, loaded automatically every session |
rules/ |
Org-wide policy: commits, code comments, security, testing, work-item format |
skills/ |
Invocable procedures, triggered by typing /skill-name |
commands/ |
Slash commands |
agents/ |
Subagent role definitions |
hooks/ |
Session lifecycle scripts, including auto-sync |
templates/ |
MR checklist, task context template |
scripts/ |
Bootstrap and checks, for example comment density in a diff |
.mcp.json |
MCP servers: issue tracker, design, library docs, error monitoring |
settings.json |
Shared settings: hooks, permissions |
.mise.toml |
Pinned runtime versions (Node, .NET, Go) |
.gitlab-ci.yml |
CI on the config itself: shell, JSON and markdown linting |
CHANGELOG.md |
History of config changes |
Rules versus skills, and why the split matters
This is the one architectural decision inside the repo that we argued about, and it comes down to the context budget.
| Content type | Where it lives | When it loads |
|---|---|---|
| Org-wide policy | rules/ |
Always, every session |
| Cross-service procedures | skills/ |
On /skill invocation |
| Structure of a specific repo | That repo’s own CLAUDE.md |
When the agent reads files there |
| Personal preferences | ~/.claude/CLAUDE.md |
Always, for that person only |
Rules load every single time, so they stay short and political: what is allowed, what is not, what is mandatory. Long procedures (how to review, how to debug, how to verify a fix) go into skills and get pulled in only when they are actually needed. Repo-specific knowledge such as “routes live here” or “entities follow this pattern” stays in the service’s own CLAUDE.md. Skip that boundary and the shared config turns into a junk drawer within a month. Ask us how we know.
Auto-sync: write the rule once, everyone has it tomorrow
A session-start hook sits in settings.json:
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sync.sh" } ] }
]
}
}
sync.sh does a fast-forward pull of the config repo. More importantly, it knows when to politely refuse:
- if you are not on
main, which means you are editing the config on a branch yourself; - if the working tree has uncommitted changes;
- if the pull failed, because you are offline or the branches diverged.
In all three cases it prints a warning and exits zero. A hook that breaks session startup is worse than no hook at all, and we learned that the fun way.
It also checks separately whether new skills appeared and warns about it. Updated rules and skills are live immediately, but brand new ones only show up after a session restart.
The payoff is small and constant: a rule changes in one merge request, and the whole team has it on their next session. No announcement, no “please update your prompt”, no Slack thread that three people miss.
Settings, MCP and runtimes come along for the ride
Once the config is shared, everything that should be identical across the team naturally moves in with it.
settings.jsonholds a baseline permission allowlist:git,glab,npm,dotnet build,dotnet test,go build,go test,docker compose. Everyone gets the same reduction in approval prompts on routine commands. Personal and machine-specific overrides go intosettings.local.json, which is gitignored..mcp.jsongives everyone the same MCP servers: issue tracker, design files, library docs search, error monitoring. Skills can then depend on them. If a skill pulls a ticket through the tracker’s MCP, it works for the whole team and not just for the person who wrote it. No secrets in the repo, tokens come from environment variables..mise.tomlpins Node, .NET and Go versions and is symlinked to the root as well, so runtime versions are described in the same repository as the rules.
What becomes possible at this level
Some of our skills are ordinary development chores that would work fine inside a single repo. The valuable ones exist precisely because the agent can see the entire workspace.
| Command | What it does |
|---|---|
/taskctx PROJ-123 |
Pulls the ticket from the tracker, snapshots it to disk, creates the task folder, moves the ticket to In Progress |
/dev-workflow |
The full gated loop: context, plan, build, test, verify, reflect |
/planning |
A phased implementation plan, persisted to disk |
/code-review |
Two-stage review: spec compliance first, then code quality |
/verification |
Blocks “it works” claims until there is fresh proof |
/refine |
Found a gap in the rules? It opens a merge request against the config repo |
Two of these are impossible inside a single repo
/taskctx. A ticket arrives from the tracker knowing nothing about repository boundaries. The skill resolves access to each service it needs: local clone in the workspace first, clone it next door if missing, and only then read the code. The context pack comes out cross-service by construction, not by anyone remembering to make it so.
/refine. The gap it fixes is discovered while working in one service, but the fix belongs in a completely different repository, the config one. From a session scoped to a single repo, that merge request simply cannot be opened. From the workspace root, the config repo is just another sibling directory.
The .work/ folder, or local memory
When work starts, /taskctx creates a local context folder at the workspace root:
.work/proj-123/
├── context.md # synthesis: goal, scope, acceptance
│ # criteria, affected repos and paths
├── snapshots/
│ ├── issue.json # the ticket and comments, verbatim
│ ├── project.json # board fields: sprint, epic, priority
│ └── gitops/ # deployment state, when applicable
└── tools/ # throwaway scripts for this task
The rules around it are simple, and they do all the work:
- Snapshots are verbatim.
issue.jsonis what the tracker actually holds, not a paraphrase. The paraphrase iscontext.md, and it marks where every fact came from. - Never invent. If a field is missing, write
UNKNOWNand list it as an open question instead of filling the gap with a plausible guess. - Throwaway scripts live here. The agent constantly needs to parse something, diff two API responses, extract candidates for a fix. That debris used to settle inside the service repos. Now it sits next to the task and never reaches a merge request.
- The folder is never committed. It is local memory, not documentation.
The practical effect is that task context survives a session restart. Coming back two days later, you do not re-explain anything. You say “continue PROJ-123” and the agent reads context.md and the snapshots.
There was one side effect we did not plan for: context.md turned out to be a decent document for humans too. When a task gets handed over, the folder gets handed over with it.
A full-stack task in one session
Ticket: “the form needs a new field, values come from the backend.” Here is how it actually runs now.
/taskctx PROJ-123. Ticket on disk, task folder created, tracker status moved.- The agent reads the OpenAPI contract in the frontend repo and the entity plus DTO in the backend, in one session, with nothing re-explained, because both are sibling directories.
- It changes the backend: migration, DTO, validation. Then regenerates the contract.
- It changes the frontend: component, validation schema, updated E2E test.
- It opens two merge requests, both following the same description template, because the MR convention is a single file in
rules/.
The point is not that the agent got smarter. The point is that it no longer has to be re-taught the system at every repository boundary, and it cannot apply one convention in the backend and a different one in the frontend, because there is physically only one convention to apply.
For larger tasks, git worktrees stack neatly on top: the agent builds in an isolated working tree next door (.wt-<task>/) while you plan the next thing in the main one.
Treat the config as a product
The most underrated piece of this whole setup is the /refine skill. It does not write product code. It fixes the config.
A reviewer flags a convention that is not written down anywhere. /refine states the gap (what happened, what should have happened, which file is at fault), edits the relevant rule, and opens a merge request against the config repo. The same comment never has to be given twice.
Which means the config repo has to be run like a real repository. Changes arrive through merge requests and review. CI lints shell scripts, JSON and markdown, because a broken hook breaks session startup for the entire team. There is a CHANGELOG.md. Every skill has a history you can read.
Ours has accumulated roughly 130 commits over five months. It is a living repository, not a folder of prompts.
What it bought us
- One source of truth. A rule exists once, in one place. There is nothing left to drift.
- Onboarding in an evening. Five
git clones, onebootstrap.sh,mise install, and a new person is on exactly the same setup as everyone else. - Reproducibility across people. The agent produces comparable results for different developers, because their configuration is literally the same revision.
- Cross-repo work became normal. Context packs, backend-plus-frontend tasks and config fixes stopped being manual glue work.
- Knowledge compounds. Every review can end in a rule instead of a verbal remark.
- The config gets reviewed like code. A bad rule is caught in a merge request, not in production inside somebody else’s session.
The honest cost
None of this is free, and we would rather you hear the downsides from us than discover them in week three.
- Startup discipline. A session opened inside a service picks up nothing. This is by far the most common failure, and it disguises itself as “the agent got worse”.
- Context budget. Everything in
rules/loads every time. The temptation to add one more paragraph is enormous, and every session pays for it. - Shared blast radius. One bad edit breaks session startup for the entire team. That is exactly why the config repo has CI and review, and why the auto-sync hook is written to silently do nothing whenever it is not sure.
- New skills need a session restart. Updated ones apply immediately, new ones do not.
- The temptation to put everything in it. The moment the shared config starts describing one service’s directory layout, it becomes a junk drawer and it will be the first thing to go stale.
- The agent sees every repo, so it can change every repo. Branch discipline, permissions and a clear commit policy matter more here than in a per-service setup.
We would pick this again. But pick it with your eyes open.
Reproduce it in an evening
- Create a
clauderepository in your organization. - Put
CLAUDE.md,rules/,skills/,settings.jsonand.mcp.jsonin it. - Write
scripts/bootstrap.shthat symlinksCLAUDE.mdand.mcp.jsoninto the workspace root. - Add
hooks/sync.shonSessionStart: ff-only pull, refuse on a dirty tree, always exit zero. - Move your first three unwritten rules into
rules/: commit convention, MR convention, and what “done” means. - Add a skill that creates
.work/<task>/from your tracker. - Put CI on the config itself, shell and markdown linting at minimum.
- Add
/refineand insist that people use it. Without the feedback loop, everything else freezes within a month.
Start with two files and one symlink. The rest gets written as real work shows you what is missing, which is, honestly, exactly how this one came to exist.