Settings Architecture
Agentic coding tools separate what the agent knows (instruction files, context) from what the agent is allowed to do (settings, permissions, hooks). Settings govern the latter. Every major tool converges on a layered scope model that lets organizations enforce policy, teams share conventions, and individuals customize their environment — all without polluting the agent’s context window.
Four-Tier Scope
Settings resolve through four layers. Each layer has a distinct owner and sharing model.
| Scope | Shared? | Purpose | Claude Code | OpenAI Codex | Gemini CLI |
|---|---|---|---|---|---|
| Managed (Org) | Admin-controlled | Enterprise policies, compliance | Managed settings (pushed by admin) | requirements.toml | Org policies |
| User (Global) | No | Personal defaults across all projects | ~/.claude/settings.json | ~/.codex/config.toml | ~/.gemini/settings.json |
| Project (Team) | Yes (committed to git) | Team-agreed rules and permissions | .claude/settings.json | .codex/config.toml | Project config |
| Local (Personal) | No (gitignored) | Machine-specific overrides, secrets | .claude/settings.local.json | Local overrides | Local config |
The key distinction: Project settings are version-controlled and shared with every team member. Local settings are gitignored and never leave the machine. This split is what makes it safe to store API keys and personal model preferences alongside team-enforced permissions.
Precedence
Settings resolve in a fixed order. When the same key appears at multiple tiers, the more specific scope wins:
graph TD
A["Managed (Org)<br/><i>lowest priority — base policy</i>"] --> B["User (Global)<br/><i>personal defaults</i>"]
B --> C["Project (Team)<br/><i>team-shared overrides</i>"]
C --> D["Local (Personal)<br/><i>highest priority — final say</i>"]
Local wins. A developer can always override a project default on their own machine. However, managed (org) settings can mark certain keys as locked, preventing downstream overrides entirely. This is how enterprises enforce hard policy (e.g., disabling network access) while still granting flexibility everywhere else.
In practice, the merge is a shallow merge per section. If your project settings define an allowedTools list and your local settings also define one, the local list replaces the project list — it does not append to it. Plan your settings accordingly.
What Goes Where
Not every setting belongs in every tier. Here is a decision framework:
| Setting | Recommended Tier | Rationale |
|---|---|---|
Permission allowlists (allowedTools, allowedMcpServers) | Project | Team-agreed; everyone on the repo should share the same permission surface. |
| Hook definitions (pre-commit lint, test gates) | Project | Deterministic automation that the whole team benefits from. |
| Environment variables, API keys | Local | Secrets must never be committed. Local is gitignored by default. |
Model preferences (model, temperature) | Local | Personal choice; one developer may prefer a smaller model for speed. |
| Cost/rate limits | User or Local | Personal spend controls that vary by individual. |
| Org compliance rules (audit logging, network restrictions) | Managed | Non-negotiable policies pushed by administrators. |
| Custom slash commands | Project or User | Project-scoped if team-specific; user-scoped if personal workflow. |
The guiding principle: if it affects the whole team, commit it. If it is personal or secret, keep it local.
settings.json Structure
Below is a complete annotated example of a project-level settings file (.claude/settings.json in Claude Code). The structure is representative of the pattern across tools, though key names differ.
{
"permissions": {
"allowedTools": [
"Read",
"Edit",
"Write",
"Bash(npm run lint)",
"Bash(npm run test)",
"Bash(npm run build)",
"mcp__github__create_pull_request",
"mcp__github__list_issues"
],
"deniedTools": [
"Bash(rm -rf *)",
"Bash(curl *)"
]
},
"hooks": {
"preCommit": [
{
"command": "npm run lint --fix",
"description": "Auto-fix lint issues before commit",
"blocking": true
},
{
"command": "npm run test -- --bail",
"description": "Run tests; abort commit on failure",
"blocking": true
}
],
"postFileEdit": [
{
"command": "npx prettier --write $FILE",
"description": "Format file after edit",
"blocking": false
}
]
},
"env": {
"NODE_ENV": "development",
"LOG_LEVEL": "debug"
},
"model": {
"default": "claude-sonnet-4-20250514",
"maxTokens": 16384
}
}
Field-by-field breakdown
permissions.allowedTools — An explicit allowlist of tools the agent may invoke without prompting. Glob patterns (e.g., Bash(npm run *)) are supported. Any tool not on this list will require user confirmation at runtime.
permissions.deniedTools — A denylist that takes precedence over the allowlist. If a tool matches both, it is denied. Use this to carve out dangerous commands from broad globs.
hooks — Named lifecycle events that trigger shell commands deterministically. The agent does not decide whether to run them; the harness executes them unconditionally at the specified point. The blocking flag controls whether a hook failure halts the operation or merely logs a warning.
env — Environment variables injected into the agent’s shell sessions. In a project settings file, these should be non-secret values. Secrets belong in the local settings file.
model — Model selection and parameter overrides. Typically set at the local or user tier so individuals can choose their preferred model without affecting the team.
Context Impact
One of the most important architectural decisions in agentic tools is the split between instruction files and settings files. They serve different purposes and have different costs.
| Instruction Files | Settings Files | |
|---|---|---|
| Examples | CLAUDE.md, AGENTS.md, codex.md | settings.json, config.toml |
| Consumed as context? | Yes — injected into the prompt | No — parsed as metadata by the harness |
| Token cost | Proportional to file size | Zero |
| Agent visibility | Agent reads and follows them | Agent does not see them directly |
| Enforcement | Soft — the model may deviate | Hard — the harness enforces mechanically |
This distinction has practical consequences:
- Do not put permission rules in instruction files. Writing “never run
rm -rf” inCLAUDE.mduses tokens and relies on the model obeying. Puttingrm -rfindeniedToolscosts zero tokens and is enforced deterministically. - Do not put hook logic in instruction files. Writing “always run lint before committing” in
CLAUDE.mddepends on model compliance. Defining apreCommithook insettings.jsonguarantees execution. - Do put coding standards in instruction files. Stylistic guidance (“use early returns”, “prefer composition over inheritance”) requires the model to understand and reason about the instruction. This is what context is for.
The rule of thumb: if a behavior can be expressed as a mechanical gate or trigger, it belongs in settings. If it requires judgment, it belongs in the instruction file.
The Enforcement Model Revisited
The three-layer enforcement model maps cleanly onto the file types:
graph TD
A["<b>Guidelines</b> — Instruction Files<br/><i>Soft, in-context, costs tokens</i><br/>Prefer named exports over default exports"]
B["<b>Rules</b> — settings.json hooks<br/><i>Deterministic, harness-executed, zero tokens</i><br/>preCommit → npm run lint --fix"]
C["<b>Gates</b> — settings.json permissions<br/><i>Hard block, cannot be bypassed, zero tokens</i><br/>deniedTools → Bash(rm -rf *)"]
A --> B --> C
Guidelines influence the model’s choices. Rules automate deterministic actions. Gates block prohibited actions. A well-configured project uses all three layers together.
Practical Example
Below is a complete project setup showing all three files working in concert. The project is a Node.js API with a PostgreSQL database.
1. Instruction file: CLAUDE.md
This file lives at the repository root and is committed to git. It consumes context tokens and provides the agent with coding guidance.
# Project Context
Node.js REST API using Express and PostgreSQL. TypeScript throughout.
## Architecture
- `src/routes/` — Express route handlers (one file per resource)
- `src/services/` — Business logic (no direct DB access in routes)
- `src/db/` — Knex query builders and migrations
- `src/types/` — Shared TypeScript interfaces
## Coding Standards
- Use early returns to reduce nesting.
- All database calls go through the service layer, never in route handlers.
- Prefer `async/await` over raw Promises.
- Every public function must have a JSDoc comment.
- Error responses use the `ApiError` class from `src/errors.ts`.
## Testing
- Tests live in `__tests__/` directories adjacent to source files.
- Use `vitest` for unit tests, `supertest` for integration tests.
- Every new route handler must have at least one integration test.
2. Project settings: .claude/settings.json
This file is committed to git. Every team member gets these permissions and hooks. It costs zero tokens.
{
"permissions": {
"allowedTools": [
"Read",
"Edit",
"Write",
"Bash(npm run lint)",
"Bash(npm run lint:fix)",
"Bash(npm run test *)",
"Bash(npm run build)",
"Bash(npm run db:migrate)",
"Bash(npx tsc --noEmit)",
"mcp__github__create_pull_request",
"mcp__github__list_issues",
"mcp__github__get_issue"
],
"deniedTools": [
"Bash(npm run db:migrate:rollback)",
"Bash(rm -rf *)",
"Bash(DROP TABLE *)",
"Bash(curl *)",
"Bash(wget *)"
]
},
"hooks": {
"preCommit": [
{
"command": "npm run lint",
"description": "Lint check before commit",
"blocking": true
},
{
"command": "npx tsc --noEmit",
"description": "Type check before commit",
"blocking": true
}
],
"postFileEdit": [
{
"command": "npx prettier --write $FILE",
"description": "Auto-format on save",
"blocking": false
}
]
},
"env": {
"NODE_ENV": "development"
}
}
3. Local settings: .claude/settings.local.json
This file is gitignored. It contains machine-specific values and overrides. Zero tokens, never shared.
{
"permissions": {
"allowedTools": [
"Bash(docker compose *)",
"Bash(psql *)"
]
},
"env": {
"DATABASE_URL": "postgresql://dev:dev@localhost:5432/myapp_dev",
"OPENAI_API_KEY": "sk-...",
"GITHUB_TOKEN": "ghp_..."
},
"model": {
"default": "claude-sonnet-4-20250514",
"maxTokens": 8192
}
}
At runtime, settings merge in order: managed → user → project → local. The project’s deniedTools still apply even with local additions. Neither settings file appears in the agent’s context — their effect is purely structural. The instruction file focuses exclusively on guidance that requires the model’s judgment.