4 The Important Files of Claude Code
4.9 Capability Files II: commands/, agents/, skills/, hooks/ |
4.10 GitHub-Ready Mini-Project: the TerpTasks Configuration Kit |
4.1 Learning Objectives
By the end of this lecture you should be able to:
Name the key files and directories Claude Code reads (CLAUDE.md, .claude/settings.json, .claude/settings.local.json, ~/.claude/settings.json, .mcp.json, ~/.claude.json, and the .claude/{agents,skills,commands,hooks}/ directories) and state each one’s purpose in a sentence.
Explain the three configuration scopes—
user, project, and local— and predict which setting wins when they conflict, using the precedence chain. Write permission rules (allow / ask / deny) in settings.json and trace how a specific tool call is evaluated against them.
Decide which file a given piece of configuration belongs in, and justify the decision in terms of audience (me vs. my team vs. the agent) and durability.
Create a custom slash command, a subagent, and a hook as files, and explain how each differs in who decides it runs (you, the model, or the harness).
Distinguish user-editable configuration from tool state (~/.claude.json) and explain why the latter stays out of version control and out of your editor.
Assemble a complete, committable .claude/ configuration kit for a real repository.
4.2 Motivation: Configuration as Code
4.2.1 Why a lecture about files?
In the last lecture you learned to operate Claude Code:
workflows, prompting, context management. But everything you typed
there was ephemeral—
4.2.2 Intuition: three questions, three kinds of files
Every file we meet today answers one of three questions:
"What should the agent KNOW?" --> context files (CLAUDE.md) |
"What MAY the agent DO?" --> policy files (settings*.json, hooks) |
"What CAN the agent USE?" --> capability files (.mcp.json, agents/, skills/, commands/) |
Keep this triad in mind; when you’re unsure where something goes, ask which question it answers.
4.2.3 The map
An annotated view of everything in play, with the two roots—
~ (your machine -- follows YOU) |
|- .claude/ |
| |- settings.json <- your global defaults |
| |- CLAUDE.md <- personal context |
| |- agents/ <- personal agents |
| |- skills/ <- personal skills |
| +- commands/ <- personal commands |
+- .claude.json <- tool STATE: |
auth, caches, trust (not yours to edit!) |
per-project state |
|
REPO (the project -- follows the TEAM) |
terptasks/ |
|- CLAUDE.md <- project context |
|- .mcp.json <- shared MCP servers |
|- .claude/ |
| |- settings.json <- shared policy (committed) |
| |- settings.local.json <- personal overrides (git-ignored) |
| |- agents/ <- project subagents |
| |- skills/ <- project skills |
| |- commands/ <- project slash commands |
| +- hooks/ <- scripts run by the harness |
+- src/ ... |
Priority guide from the reference handout, which we’ll follow:
File | Purpose | Scope |
CLAUDE.md | persistent instructions about the project | project (or user) |
.claude/settings.json | shared config: permissions, hooks, env | project |
.claude/settings.local.json | personal settings, not committed | local |
~/.claude/settings.json | global defaults for all projects | user |
.mcp.json | project MCP server definitions | project |
~/.claude.json | tool state: auth, MCP, trust, caches | user |
.claude/agents/ | custom subagents | project/user |
.claude/skills/ | reusable skills | project/user |
.claude/commands/ | custom slash commands | project/user |
.claude/hooks/ | scripts run around tool execution | project/user |
4.3 Running Example: TerpTasks Gets a Team
Last lecture, TerpTasks was yours alone; you wrote its CLAUDE.md and fixed the sorting bug. The story continues: two teammates are joining, and the project is moving to a shared GitHub repo. Overnight, questions appear that never mattered solo:
Alice auto-approved pytest on her machine—
how does Bob get that without clicking “allow” forty times? → shared settings.json Bob likes verbose model output; Alice hates it. → settings.local.json
Nobody, ever, should let the agent read .env (it now holds a database password). → deny permission, committed
The team wants the agent to file GitHub issues. → .mcp.json
Every PR should get the same style of first-pass review. → a shared subagent
By the end of the lecture, we will have built the complete
configuration kit that answers all five—
4.4 The Scope Model and Precedence
Before individual files, the one concept that organizes them all.
Every Claude Code setting exists at a scope: user (~/.claude/, applies to all your projects), project (checked into the repo, applies to everyone who clones it), or local (in the repo but git-ignored, applies to you in this repo only). When scopes conflict, the more specific scope wins; deny rules win regardless.
In plain English: global defaults for you everywhere; committed files for everyone here; local files for you, here.
Precedence chain (highest wins):
enterprise managed policy (IT department; you can't override) |
| overrides |
command-line flags (this invocation only) |
| overrides |
.claude/settings.local.json (you, this project) |
| overrides |
.claude/settings.json (the team, this project) |
| overrides |
~/.claude/settings.json (you, everywhere) |
Intuition. The setting closest to the work wins. The enterprise layer at top is new but sensible: your employer’s security team gets the final word, which is exactly what makes companies willing to allow these tools at all.
Misconception. “Project settings override my local
ones—
Worked example. Alice’s three files contain:
~/.claude/settings.json: { "permissions": { "allow": ["Bash(git status)"] } } |
terptasks/.claude/settings.json: { "permissions": { "allow": ["Bash(pytest -q)"], |
"deny": ["Read(.env)"] } } |
terptasks/.claude/settings.local.json: |
{ "permissions": { "allow": ["Bash(python -m terptasks.cli *)"] } } |
Trace four tool calls the agent attempts:
Attempted call | Matching rule | Source | Result |
Bash(git status) | allow | user scope | runs without prompting |
Bash(pytest -q) | allow | project scope | runs without prompting |
Bash(python -m terptasks.cli list) | allow (wildcard) | local scope | runs -- only for Alice |
Read(.env) | deny | project scope | blocked, even with a local allow |
The union of allows applies; deny trumps everything. Bob clones the
repo and immediately inherits rows 2 and 4—
4.5 Context Files: CLAUDE.md
CLAUDE.md is a Markdown file
automatically loaded into the model’s context at session start. It
exists at multiple scopes—
In plain English: the project’s AI playbook: architecture, commands, conventions, things to avoid. We covered how to write one last lecture; today we place it in the file ecosystem.
Intuition. CLAUDE.md is a context file—
Common misconceptions.
“CLAUDE.md and settings.json overlap.” They never do: prose the model reads vs. JSON the harness enforces. Advice vs. law.
“One CLAUDE.md per machine.” They stack: your ~/.claude/CLAUDE.md (“I prefer type hints”) loads alongside each project’s. Keep personal style user-scoped; keep project facts project-scoped.
Worked example—
Two features worth knowing:
Imports: a line like @docs/architecture.md inside CLAUDE.md pulls another file into context—
keep CLAUDE.md short and link out. Subdirectory files: a CLAUDE.md inside tests/ loads when the agent works on files there—
put test-specific conventions next to the tests.
4.6 Policy Files: the settings.json Family
4.6.1 .claude/settings.json— the team’s rules
A JSON file, committed to the repository, that configures harness behavior for every session in this project: tool permissions, environment variables, hooks, model choice, and other options.
In plain English: “What is Claude allowed to do here, for everyone on the team?”
TerpTasks’ shared policy (answering questions 1 and 3 from the running example):
{
"permissions": {
"allow": [
"Bash(pytest -q)",
"Bash(pytest tests/*)",
"Bash(ruff check:*)",
"Bash(ruff format:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Bash(git push:*)"
]
},
"env": {
"TERPTASKS_ENV": "development"
}
}Reading permission rules. Each rule is Tool or Tool(specifier). For Bash, the specifier matches the command: Bash(git diff:*) allows git diff with any arguments; bare Bash(git status) allows exactly that command. For file tools, the specifier is a path pattern: Read(.env) matches the project’s .env.
Step-by-step trace. The agent wants to run git push origin main:
Harness checks deny first → Bash(git push:*) matches → blocked, with the reason shown to the model.
The model sees the denial and must adapt (typically: it asks you to push).
Now ruff check src: deny list → no match; allow list → Bash(ruff check:*) matches → runs with no prompt. A command matching neither list falls back to the default: ask the human. So the three verdicts are allow / ask / deny, and “ask” is the safety net for everything you didn’t anticipate.
Why deny git push? The team decided pushes are human-only actions This is that value, encoded as policy instead of vigilance.
Misconception. “Allow rules are dangerous.” An
overly-broad allow (Bash(*)) is dangerous. Precise allows are
the opposite: they remove prompt fatigue for known-safe commands so
your attention is saved for the calls that genuinely need review.
Prompt fatigue—
4.6.2 .claude/settings.local.json— your exceptions
Same schema, different audience: this file is for you in this repo, and Claude Code adds it to .gitignore automatically when it creates it.
Worked example. Bob is experimenting with the SQLite migration and got tired of approving sqlite3 calls:
{
"permissions": {
"allow": ["Bash(sqlite3 dev.db *)"]
}
}This belongs local, not shared, because the team hasn’t adopted
SQLite—
4.6.3 ~/.claude/settings.json— your defaults everywhere
Same schema again, applying to all your projects. Good candidates:
"Bash(git status)", "Bash(ls:*)"—
4.6.4 Which layer? A decision procedure
Is it a fact/instruction in prose? ------------> CLAUDE.md (project or user) |
Is it enforcement (permissions/hooks/env)? |
| |
|- Should teammates inherit it? -----------> .claude/settings.json (commit) |
|- Just me, just this repo? -----------> .claude/settings.local.json |
+- Me, every repo? -----------> ~/.claude/settings.json |
4.7 Capability Files I: .mcp.json
.mcp.json, at the repository root
and typically committed, declares project-scoped MCP
servers—
In plain English: “What external tools can Claude use in this project?” In the MCP lecture: servers expose tools over a standard protocol; Claude Code is a client. This file is where a project pins its servers so the whole team shares them.
TerpTasks wants the agent to file GitHub issues (question 4 from the running example):
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}Three teaching points in ten lines:
${GITHUB_TOKEN}—
environment-variable expansion. The reference to a secret is committed; the secret itself lives in each developer’s shell. Never inline tokens in this file— it’s in Git. Trust prompt. The first time you open a project with an .mcp.json, Claude Code asks whether you trust its servers—
because a malicious repo could otherwise hand your agent malicious tools the moment you open it. That approval is recorded per-project in ~/.claude.json (The State File: ~/.claude.json). Scope, again. MCP servers can also be configured user-wide (claude mcp add –scope user) for tools you want everywhere; .mcp.json is specifically the shared project scope.
Misconception. “MCP config goes in settings.json.” Separate file, separate concern: settings.json is what the agent may do; .mcp.json is what it can reach. (History note: user-scoped MCP servers actually live in ~/.claude.json, which is one reason that file exists.)
4.8 The State File: ~/.claude.json
A JSON file in your home directory where Claude Code persists its own state: authentication/session data, user-scoped MCP servers, per-project trust decisions and permission history, and caches.
In plain English: the tool’s memory about you and your
machine—
Intuition. Every serious CLI has one of these (~/.gitconfig is half config, half state; ~/.docker/config.json stores auth). The design rule it teaches: separate what humans author from what machines persist. Human-authored files are small, diffable, committable. State files are none of those.
Practical implications.
Don’t hand-edit it; use commands (claude mcp add, /login) that write it for you.
Never commit it or paste it into an issue—
it contains credentials. Debugging tip: if a project stopped prompting for trust, or an old permission decision haunts you, this is where that state lives.
Misconception. “~/.claude.json vs
~/.claude/settings.json—
4.9 Capability Files II: commands/, agents/, skills/, hooks/
Four directories, one organizing question: who decides this runs?
Directory | Contains | Who triggers it |
.claude/commands/ | prompt templates | you (type /name) |
.claude/agents/ | subagent definitions | the model (delegates) or you |
.claude/skills/ | skills (instructions + resources) | the model (matches your request) |
.claude/hooks/ | shell scripts | the harness (deterministic, on events) |
Each exists at project scope (committed, team-shared) and user scope (~/.claude/..., personal). We’ll build one of each for TerpTasks.
4.9.1 .claude/commands/— reusable prompts (you trigger)
A Markdown file per command; the body is the prompt, optional YAML frontmatter adds metadata, and $ARGUMENTS interpolates what you type after the command name. Last lecture’s /release-check was one. A second example, fix-issue.md:
---
description: Fix a GitHub issue end-to-end
argument-hint: <issue-number>
---
Fix GitHub issue #$ARGUMENTS in this repository:
1. Read the issue with the GitHub MCP tools.
2. Reproduce the problem with a failing test.
3. Fix it; run `pytest -q` until green.
4. Summarize the change and reference the issue number.Now /fix-issue 42 expands to the full checklist with 42 substituted. Note how it composes with earlier files: step 1 works because .mcp.json provides GitHub tools; step 3’s pytest runs unprompted because settings.json allows it. The kit is a system, not ten isolated files.
4.9.2 .claude/agents/— subagents (the model delegates)
A subagent is a Markdown file with frontmatter (name, description, allowed tools, optionally a model) whose body is that agent’s system prompt. Subagents run with their own context window and can be tool-restricted.
TerpTasks’ shared reviewer (question 5 from the running example), .claude/agents/code-reviewer.md:
---
name: code-reviewer
description: Reviews diffs for bugs, missing tests, and convention
violations. Use proactively after significant code changes.
tools: Read, Grep, Glob, Bash(git diff:*), Bash(pytest:*)
---
You are the TerpTasks code reviewer. For the diff you are given:
1. Check conformance with CLAUDE.md conventions (timezone-aware UTC,
type hints, tests for behavior changes).
2. Look for bugs and missing edge cases, especially around datetime
comparisons.
3. Report findings as a prioritized list: blocker / should-fix / nit.
Do not edit any files. You review; the main agent fixes.Walk the design: the description tells the main agent
when to delegate (“use proactively...”); the tools line is
least privilege—
4.9.3 .claude/skills/— packaged know-how (the model matches)
A skill is a directory containing a SKILL.md
(frontmatter: name + description; body: instructions) plus any
supporting files (scripts, templates, references). The crucial
mechanic is progressive disclosure: at session start only
names and descriptions are in context; the model loads a skill’s
full body only when your request matches it. That’s why
skills scale to dozens without burning your context window—
A TerpTasks example, .claude/skills/db-migration/SKILL.md:
---
name: db-migration
description: Write and verify TerpTasks database schema migrations.
Use when the user asks to change the task schema or storage layer.
---
When writing a TerpTasks migration:
1. Migrations live in migrations/, numbered NNN_description.sql.
2. Every migration needs a paired rollback in the same file under
a `-- rollback` marker.
3. After writing one, apply it to a scratch DB and run `pytest -q`.
4. Never modify an already-committed migration; add a new one.4.9.4 Hooks— deterministic automation (the harness triggers)
Hooks close the loop we opened last lecture (“preferences go in
CLAUDE.md; requirements go in hooks”). Mechanically: hook
configuration lives in settings.json (so it rides the
same scope/precedence system), and the scripts it invokes
conventionally live in .claude/hooks/. Hooks fire on lifecycle
events—
TerpTasks enforces formatting the guaranteed way. In .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command",
"command": ".claude/hooks/format.sh" }
]
}
]
}
}And .claude/hooks/format.sh:
#!/bin/bash
# Reads hook JSON on stdin; formats the file that was just edited.
file=$(jq -r '.tool_input.file_path // empty')
if [[ "$file" == *.py ]]; then
ruff format "$file" >/dev/null 2>&1
fi
exit 0Trace: agent edits store.py → harness fires
PostToolUse → matcher Edit|Write matches → script runs →
ruff format store.py. Every time, whether or not the model
remembered, because the harness—
Security note. Hooks are arbitrary code executing with your
privileges on model-triggered events. Review them in cloned repos
exactly as skeptically as you’d review a Makefile before running
make—
4.10 GitHub-Ready Mini-Project: the TerpTasks Configuration Kit
Everything from the preceding sections assembled into a committable
overlay for last lecture’s terptasks repository. (Only
settings.local.json stays out of Git—
4.10.1 Structure
terptasks/ |
|- CLAUDE.md # from last lecture, +one line for migrations |
|- .mcp.json # GitHub MCP server |
|- .claude/ |
| |- settings.json # team permissions + hooks |
| |- settings.local.json # EXAMPLE ONLY -- each dev writes their own |
| |- commands/ |
| | |- release-check.md # from last lecture |
| | +- fix-issue.md |
| |- agents/ |
| | +- code-reviewer.md |
| |- skills/ |
| | +- db-migration/ |
| | +- SKILL.md |
| +- hooks/ |
| +- format.sh # chmod +x |
+- src/, tests/, pyproject.toml # unchanged from last lecture |
4.10.2 The complete settings.json
Merging the permissions and hooks shown earlier:
{
"permissions": {
"allow": [
"Bash(pytest -q)",
"Bash(pytest tests/*)",
"Bash(ruff check:*)",
"Bash(ruff format:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Bash(git push:*)"
]
},
"env": {
"TERPTASKS_ENV": "development"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/format.sh" }
]
}
]
}
}All other files appear verbatim in the sections above. Setup on a fresh clone:
git clone <your-fork-url> terptasks && cd terptasks
chmod +x .claude/hooks/format.sh # ensure the hook is executable
claude # trust prompt appears for .mcp.json -> review, accept4.10.3 Verifying the kit (the “tests” of a config project)
Configuration deserves verification like any code. From inside a session:
Check | How | Expected |
permissions loaded | /permissions | allow/deny lists visible with their sources |
deny works | ask Claude to read .env | blocked by rule, not by the model's politeness |
allow works | ask Claude to run pytest -q | runs with no approval prompt |
hook fires | ask for a deliberately mis-formatted edit to store.py | file is ruff-formatted on disk afterward |
MCP up | /mcp | github server listed as connected |
command works | /fix-issue 1 | expands with 1 substituted |
agent visible | /agents | code-reviewer listed from project scope |
4.11 Practice Exercises
4.11.1 Basic
E1. For each of the three questions in the motivation (know / may do / can use), name every file from the priority table that answers it.
E2. Without looking, write the settings precedence chain from highest to lowest. Then check yourself against The Scope Model and Precedence and note anything you inverted.
E3. Classify each item into CLAUDE.md, settings.json, settings.local.json, or ~/.claude/settings.json, with one-sentence justifications: (a) “we use Poetry, not pip”; (b) allow poetry run pytest; (c) “I personally prefer seeing diffs before edits are applied”; (d) deny reading secrets/; (e) allow git status in every repo you own.
E4. Alice’s local file allows Bash(rm -rf build); the project file denies Bash(rm:*). What happens when the agent tries rm -rf build in Alice’s session, and why?
E5. Explain to a teammate, in three sentences, why ~/.claude.json should never appear in a Git repository even though .claude/settings.json should.
E6. Build the GitHub-Ready Mini-Project: the TerpTasks Configuration Kit kit onto your terptasks fork and run the entire verification table, capturing evidence (screenshots or transcript lines) for each row.
E7. Write a PreToolUse hook that blocks any Edit or Write whose target path is under migrations/, exiting with code 2 and a clear message. Demonstrate the block and explain why this beats a CLAUDE.md sentence saying the same thing.
E8. Create a personal (~/.claude/commands/) command /standup that summarizes your last day of Git activity across any repo. Then explain in writing why this one belongs at user scope while /release-check belongs at project scope.
E9. Design a test-engineer subagent for TerpTasks: write the full frontmatter (with a least-privilege tools: line) and system prompt. Justify every tool you granted—
and every one you withheld. E10. Take the db-migration skill and deliberately break its description (make it vague: “helps with databases”). Verify the skill stops firing for a schema-change request, then fix it. What does this teach about how skills are matched?
E11. Config-review lab. Trade configuration kits with another student. Audit theirs for: over-broad allows, missing denies, hooks that could be exploited, and secrets that leaked into committed files. Write findings as blocker/should-fix/nit—
using their own code-reviewer agent’s rubric against them is encouraged. E12. The malicious-repo thought experiment. You clone an untrusted repository containing .claude/settings.json, .mcp.json, hook scripts, and a CLAUDE.md. Enumerate, file by file, what each could attempt to do to you, at what moment (clone? first claude launch? first edit?), and which layer of Claude Code’s design (trust prompts, git-ignored local files, permission asks, enterprise policy) mitigates each. Conclude with the one file you’d inspect first and why.
E13. Org policy design. You are the security lead for a 30-developer company adopting Claude Code. Design the enterprise managed-policy layer plus a recommended project template: what is denied everywhere and why; what is allowed everywhere; what is left to project scope; how hooks enforce your audit requirements. Deliver as a 1–2 page policy document plus the actual JSON.
4.12 Summary
4.12.1 Key takeaways
Three questions organize everything: context files say what the agent knows (CLAUDE.md), policy files say what it may do (settings*.json, hooks), capability files say what it can use (.mcp.json, agents, skills, commands).
Scope is the master concept: user → project → local, with more-specific winning and deny winning absolutely. It’s Git config layering, reborn.
Prose advises; JSON enforces. “Don’t read .env” in CLAUDE.md is a request; "deny": ["Read(.env)"] is a guarantee.
The four directories differ by trigger: commands (you), agents (delegation), skills (matching), hooks (the harness, deterministically).
Committed config is team infrastructure—
reviewed in PRs, inherited on clone. settings.local.json is the experiment lane; good rules graduate. State ≠ config: ~/.claude.json belongs to the tool. Don’t edit it, never commit it.
Config from strangers is code from strangers. Trust prompts exist for .mcp.json and hooks because a cloned repo configures your agent.
4.12.2 Terminology
Term | Meaning |
scope | where a setting lives: user, project, or local |
precedence | conflict resolution: enterprise > CLI > local > project > user |
permission rule | allow/ask/deny pattern like Bash(git diff:*) |
memory file | CLAUDE.md at any scope; they stack rather than override |
import | an @path line in CLAUDE.md pulling another file into context |
MCP server | external tool provider declared in .mcp.json or user scope |
state file | ~/.claude.json: auth, trust, caches -- tool-owned |
subagent | frontmatter + system prompt in agents/; own context, restricted tools |
skill | directory with SKILL.md; loaded on demand via its description |
progressive disclosure | only skill names/descriptions in context until one matches |
hook | harness-executed script on lifecycle events; can block tool calls |
least privilege | granting a component only the tools it needs |
4.12.3 Common mistakes
Putting enforcement wishes in CLAUDE.md and wondering why they’re occasionally ignored.
Committing settings.local.json (or worse, secrets in .mcp.json).
Bash(*)-style allows that eliminate the ask layer entirely.
Editing ~/.claude.json by hand instead of using claude mcp add / /login.
Vague skill descriptions, so the skill never fires (or fires constantly).
Granting subagents Edit when their job is to review.
Trusting a cloned repo’s .claude/ contents without reading them.
4.12.4 Connections
Backward: this lecture is the durable half of How to Effectively Use Claude Code; CLAUDE.md and /release-check from there now sit inside a full kit. The scope chain mirrors the git lecture’s config layering; hooks-as-enforcement mirror CI merge gates.
Forward (MCP lecture): .mcp.json is the client-side view of the servers you’ll build.
Forward (security): E12’s threat model—
configuration as an attack surface— returns when we discuss supply-chain risks alongside the OWASP material.