Optimize Claude Code Context Window
Cut context bloat in Claude Code with hooks, subagents, and lean config.
Introduction
30 Second Summary
Ever had a long conversation with an AI coding assistant only for it to "forget" what you discussed five minutes ago? That's what happens when a context window fills up with noise instead of signal.
In this project, you will systematically optimize your Claude Code environment to keep context lean and high-signal across long coding sessions.
What You'll Build
A complete Claude Code optimization configuration for your real codebase, using layered techniques from exclusion files to custom subagents and preprocessing hooks.
By the end of this project, you'll have:
- A .claudeignore file and lean CLAUDE.md with path-scoped rules that load only when Claude works with matching files.
- A skills architecture that delivers domain knowledge on-demand without always-on context cost.
- A custom subagent that isolates verbose tasks in a separate context window, plus a preprocessing hook that filters noisy output before it enters your context.
- Secret Mission: Build a SessionStart hook that injects dynamic project state (current branch, recent commits, open issues) into every new session automatically.
Are there any prerequisites?
You'll need an active Claude Code Max subscription, an existing codebase you want to optimize, and basic familiarity with the Claude Code CLI (running commands, chatting with Claude in the terminal).
Audit Your Current Context Footprint
Your context window is finite. When it fills up, Claude Code forgets instructions, gives worse responses, and you hit rate limits faster on your Max subscription.
Before you can fix any of this, you need to know exactly what's filling your context right now. In this step, you'll run built-in diagnostic commands to capture a measurable baseline that you can compare against after each optimization.
In this step, get ready to:
- Run diagnostic commands to capture your context window baseline.
- Identify the top context consumers in your project.
- Set optimization targets for the rest of the project.
Run /context and /usage for your baseline
Claude Code provides built-in slash commands that reveal exactly how much of your context window is in use. These are your starting measurements.
- In your Claude Code session, visualize your current context window utilization by running:
/context
You should see a colour-coded grid. Green represents available space, yellow means moderately used, and red means nearly full.
- Note the color distribution you see. Is the grid mostly green, mostly yellow, or hitting red?
- Check your token statistics by running:
/usage
What does /usage show?
/usage shows your plan usage limits and token statistics. On a Max subscription, you won't be billed per-token, but these numbers reveal how quickly you're consuming your context budget per session.
- Record both the color distribution from /context and the token numbers from /usage. These are your baseline measurements to compare against later.
Not seeing output from these commands?
Make sure you're typing the commands inside a Claude Code session, not a regular terminal shell. The / prefix only works within Claude Code's interactive prompt.
If Claude Code isn't running yet, start it by typing claude in your terminal from your project's root directory.
Help me get /context and /usage working in my Claude Code session
Identify what's consuming context
Now that you have the big picture, drill into the specific sources of context consumption. Large instruction files, MCP servers, and missing exclusion patterns are the most common culprits.
- Count the lines in your existing CLAUDE.md by running:
wc -l CLAUDE.md
Why does CLAUDE.md size matter?
CLAUDE.md loads into context at the start of every session. A file over 200 lines adds significant tokens before you even ask your first question.
The official recommendation is to keep it under 200 lines. Anything beyond that likely contains instructions Claude rarely needs.
- Check what MCP servers are currently registered by running:
/mcp
How do MCP servers affect context?
Each MCP server registers tool definitions that enter your context window. MCP tool definitions are deferred by default, meaning only tool names load until Claude uses a specific tool.
However, many servers still add meaningful overhead. CLI tools like gh, aws, and gcloud are more context-efficient than MCP servers because they don't add any per-tool listing.
- Check whether you already have a .claudeignore file by running:
ls .claudeignore
If the file doesn't exist, that's a significant finding. Without a .claudeignore, Claude can read every file in your project, including build artifacts and dependencies.
- Check your session cost breakdown by running:
/cost
What does /cost show?
/cost shows the total API cost and time elapsed for your current session, broken down by model and token type. This helps you understand which operations are most expensive in token terms.
Getting 'No such file' for CLAUDE.md?
If you don't have a CLAUDE.md yet, that's fine. Your line count is 0, and you'll create one in a later step.
Make sure you're running wc -l CLAUDE.md from your project root directory, not a subdirectory.
Help me find my CLAUDE.md or understand why it doesn't exist
Establish your optimization targets
With your diagnostic data collected, you can now identify the specific areas where you'll focus your optimization efforts.
- Review your findings from the previous substeps and identify the top 3 sources of context bloat in your project.
Common sources of context bloat include:
- A CLAUDE.md over 200 lines that loads instructions Claude rarely needs.
- No .claudeignore file, allowing Claude to read build artifacts and dependency directories.
- Multiple MCP servers registering tool definitions that add per-tool overhead.
- Verbose test or log output that floods context during inline execution.
- Write down your top 3 bloat sources. You'll tackle each one systematically in the remaining steps.
Prioritize by token impact
Focus on always-on costs first. A bloated CLAUDE.md or missing .claudeignore consumes tokens in every single session. Occasional verbose output only matters during specific operations.
You now have a concrete baseline to measure against. Next up, you'll start eliminating bloat by configuring your .claudeignore and trimming your CLAUDE.md to under 200 lines.
Configure .claudeignore and Lean CLAUDE.md
In Step 1, you identified where your context tokens are going. Now it's time to cut the fat.
The most impactful first move is telling Claude what NOT to look at. Every file Claude indexes or reads eats into your context window. By excluding irrelevant files and trimming your instructions down to essentials, you free up space for the high-signal content that actually improves responses.
In this step, get ready to:
- Create a .claudeignore file that blocks irrelevant files from entering context.
- Trim your CLAUDE.md to under 200 lines of essential-only instructions.
- Move domain-specific rules into path-scoped files that load only when relevant.
Create your .claudeignore file
A .claudeignore file works like .gitignore but for Claude's file indexing. Any path matched by a pattern in this file becomes invisible to Claude. It won't index it, read it, or waste tokens on it.
- In your code editor, create a new file called .claudeignore in your project root (the same directory where your CLAUDE.md lives).
- Paste the following content into the file:
# Dependencies
node_modules/
.venv/
vendor/
# Build artifacts
dist/
build/
.next/
out/
coverage/
__pycache__/
# Generated files
*.lock
*.min.js
*.min.css
*.map
# Large data
*.sqlite
*.db
data/
# IDE and OS
.idea/
.vscode/
.DS_Store
What does this file do?
Each section targets a different category of context bloat:
- Dependencies (node_modules/, .venv/) can be thousands of files Claude never needs to read directly.
- Build artifacts (dist/, coverage/) are generated output that duplicates your source code.
- Generated files (*.lock, *.map) are machine-written and add noise without signal.
- Large data files (*.sqlite, data/) consume massive token counts with no benefit to code understanding.
- Save .claudeignore.
- Confirm the file exists by running this in a separate terminal tab:
ls -la .claudeignore
You should see .claudeignore listed with its file size.
Restart your session for changes to take effect
After creating .claudeignore, exit your current Claude Code session and start a new one for the exclusions to apply. The file affects context loading at session start.
File not showing up?
Files starting with a dot are hidden by default in most file browsers. Use ls -la (not ls) to see hidden files. Also confirm you created the file in your project root, not inside a subdirectory.
help me figure out why my .claudeignore isn't being detected
✔️ Awesome, I've got everything!
Great. Make sure you've saved .claudeignore before moving on.
ⓧ I'd like to double check the full code
# Dependencies
node_modules/
.venv/
vendor/
# Build artifacts
dist/
build/
.next/
out/
coverage/
__pycache__/
# Generated files
*.lock
*.min.js
*.min.css
*.map
# Large data
*.sqlite
*.db
data/
# IDE and OS
.idea/
.vscode/
.DS_Store
Trim your CLAUDE.md to under 200 lines
Your CLAUDE.md loads into context with every single message you send. A bloated instructions file quietly eats hundreds of tokens per exchange, pushing useful context out of the window.
The target is under 200 lines. Longer files consume more context and reduce Claude's adherence to the instructions they contain.
- Open your CLAUDE.md in your code editor.
- For each line, apply the include/exclude test: "Would removing this cause Claude to make mistakes?" If the answer is no, cut it.
What should stay in CLAUDE.md?
Keep only these five categories:
- Build commands Claude can't guess (custom scripts, non-standard test runners).
- Code style rules that differ from language defaults (naming conventions, import order preferences).
- Testing instructions (how to run tests, what frameworks are used, where test files live).
- Architectural decisions Claude needs to respect ("we use Server Components by default", "state lives in Zustand").
- Common gotchas that would otherwise cause repeated mistakes.
Everything else (generic best practices, obvious conventions, documentation Claude can infer from code) is noise. Cut it aggressively.
- Save your trimmed CLAUDE.md.
- Verify your line count by running:
wc -l CLAUDE.md
You should see a number under 200. If you're still over, look for sections that repeat information or state obvious conventions.
Struggling to get under 200 lines?
If your CLAUDE.md has domain-specific instruction blocks (deployment checklists, migration procedures, PR review steps), don't delete them yet. The next substep moves these into separate files that load only when needed.
help me decide what to cut from my CLAUDE.md
Move domain-specific rules into path-scoped files
Path-scoped rules live in .claude/rules/ and include a paths frontmatter field. Claude only loads these rules into context when it accesses files matching those paths. This means database rules consume zero tokens when you're working on frontend code.
- Create the rules directory by running this command in your terminal:
mkdir -p .claude/rules
- Confirm the directory exists by running:
ls .claude/rules
You should see an empty directory listed (no error output).
- Create a new file called db-rules.md inside .claude/rules/ with the following content:
---
paths:
- "db/**"
- "migrations/**"
- "**/models/**"
---
# Database Rules
- Always create reversible migrations
- Use transactions for multi-step schema changes
- Test migrations against a copy of production schema before merging
- Name migrations with timestamp prefix: YYYYMMDD_HHMMSS_description
What does the paths frontmatter do?
The paths array uses glob patterns. When Claude opens or modifies a file matching any pattern in the list, these rules automatically load into context. When Claude works on unrelated files, these rules cost zero tokens.
In this example, the database rules activate whenever Claude touches files in db/, migrations/, or any models/ directory. This is far more efficient than keeping these instructions in your always-loaded CLAUDE.md.
- Save .claude/rules/db-rules.md.
- Verify the file was created correctly by running:
cat .claude/rules/db-rules.md
You should see the full file contents printed, including the YAML frontmatter at the top with the three path patterns.
Adapt this to your own project
The db-rules.md file is an example. Think about what domain-specific instructions you pulled out of your CLAUDE.md in the previous substep. Create additional rule files for each domain (e.g., api-rules.md with paths matching your API routes, or test-rules.md scoped to your test directories).
Rules not loading when expected?
Path-scoped rules fire based on file access patterns. If Claude hasn't opened or modified a file matching your glob patterns in the current session, the rules won't load. Double-check that your patterns match actual paths in your project.
help me debug why my path-scoped rules aren't loading
✔️ Awesome, I've got everything!
Great. Double check you've saved .claude/rules/db-rules.md and that your CLAUDE.md is under 200 lines.
ⓧ I'd like to double check the full code
---
paths:
- "db/**"
- "migrations/**"
- "**/models/**"
---
# Database Rules
- Always create reversible migrations
- Use transactions for multi-step schema changes
- Test migrations against a copy of production schema before merging
- Name migrations with timestamp prefix: YYYYMMDD_HHMMSS_description
Your context window just got a lot leaner. Next up, you'll extract workflow-specific instructions into on-demand skills that cost zero tokens until you actually need them.
Build Your Skills Architecture
Your CLAUDE.md is lean and your path-scoped rules load only when Claude touches matching files. The next layer targets workflow-specific instructions that sit in CLAUDE.md but only matter during specific tasks.
Skills are standalone instruction files that Claude loads on-demand. Moving specialized processes out of CLAUDE.md into skills reduces your base context cost to zero for that knowledge until you explicitly invoke it.
In this step, get ready to:
- Extract workflow-specific instructions from CLAUDE.md into skill files.
- Configure manual-only invocation to eliminate automatic context cost.
- Test on-demand skill loading and compare context usage.
Identify and create skill files from your CLAUDE.md
Your CLAUDE.md likely contains multi-step processes that only apply during specific workflows. Deployment checklists, PR review standards, and database migration steps are all great candidates to move into skills.
- Look through your CLAUDE.md and identify 2-3 instruction blocks that are workflow-specific. Good candidates are processes you only need occasionally, like a deployment checklist, a PR review process, or a testing workflow.
- Create the skills directory by running this command:
mkdir -p .claude/skills
- Confirm the directory exists by running:
ls .claude/skills
You should see an empty directory listed with no errors.
What makes a good skill candidate?
Apply this test to each section of your CLAUDE.md: would removing this cause Claude to make mistakes on everyday tasks? If the answer is no, it belongs in a skill. Deployment processes, release checklists, and code review standards are occasional workflows. Build commands, code style rules, and architectural decisions are everyday context that should stay in CLAUDE.md.
Create a deployment skill with manual-only invocation
Each skill is a markdown file with YAML frontmatter that controls when Claude loads it. The disable-model-invocation: true flag prevents Claude from automatically loading the skill, reducing its context cost to zero until you explicitly call it.
- Create a file called deploy.md inside .claude/skills/ with the following content:
---
name: deploy
description: Production deployment checklist and process
disable-model-invocation: true
---
# Deployment Process
1. Run full test suite: ensure all tests pass
2. Check for uncommitted changes: git status
3. Tag the release: git tag -a v{version} -m "Release v{version}"
4. Push with tags: git push origin main --tags
5. Monitor deployment logs for errors in the first 5 minutes
6. Verify health check endpoint responds 200
What does this frontmatter do?
- name sets the display name and becomes the slash command you type (/deploy).
- description tells Claude what the skill does. Claude uses this to decide when the skill is relevant.
- disable-model-invocation: true hides the skill entirely from Claude's automatic invocation. It also prevents the skill from being preloaded into subagents. The skill's context cost is zero until you manually call it with /deploy.
- Save deploy.md.
- Create 1-2 additional skill files for your own workflows using the same frontmatter structure. For example, you might create a pr-review.md or migrate-db.md skill based on content you removed from your CLAUDE.md.
Skill not showing up?
Skills added as files on disk require a session restart to be discovered. If you just created the file and it does not appear when you type /, exit Claude Code and start a new session. Skills created through the legacy .claude/commands/ path also work but skills in .claude/skills/ are recommended.
help me debug why my skill file isn't being discovered
Test your skills and compare context cost
With your skills created, you can now see the context savings compared to keeping the same content in CLAUDE.md.
- In your Claude Code session, run /context to check the current context utilization. Note how much green (available) space you have before invoking any skill.
- Invoke your deploy skill by typing /deploy in the Claude Code prompt.
- Verify that Claude loads the deployment instructions and responds with the checklist content.
- Run /context again after invocation to see how much context the skill consumed.
On-demand vs always-on context cost
When disable-model-invocation is true, the skill's description and content are completely hidden from Claude until you invoke it. The context cost is zero between invocations.
Compare this to the same instructions living in CLAUDE.md, where they load into context at the start of every session regardless of whether you need them. Over a long session, this difference compounds as auto-compaction preserves CLAUDE.md content but discards conversation history.
- Test any additional skills you created by invoking them with / followed by the skill name.
Seeing no difference in /context?
If the context grid looks the same before and after, your skill content may be very short. The savings scale with content length. A 6-line skill saves less than a 50-line deployment runbook. The real value appears over time as you move more workflow-specific content out of CLAUDE.md.
help me understand why my context usage looks the same after moving content to skills
✔️ Awesome, I've got everything!
Great. Double check you have saved your deploy.md file and any additional skills you created.
ⓧ I'd like to double check the full code
Here is the complete .claude/skills/deploy.md file:
---
name: deploy
description: Production deployment checklist and process
disable-model-invocation: true
---
# Deployment Process
1. Run full test suite: ensure all tests pass
2. Check for uncommitted changes: git status
3. Tag the release: git tag -a v{version} -m "Release v{version}"
4. Push with tags: git push origin main --tags
5. Monitor deployment logs for errors in the first 5 minutes
6. Verify health check endpoint responds 200
Your skills architecture is in place. Specialized knowledge now loads only when you need it, keeping your base context clean. Next up, you will create a custom subagent that isolates verbose tasks like test output into their own context window.
Create a Custom Subagent for Context Isolation
Your skills and rules now load knowledge on-demand. But there's still a major source of context bloat: verbose task output.
Running your test suite, tailing logs, or reading large documentation files can dump thousands of tokens into your main conversation. You only need the summary, but the full output stays in context forever. Subagents solve this by running in their own isolated context window and returning only a concise result.
In this step, get ready to:
- Create a custom subagent using the /agents command.
- Write a system prompt that filters verbose output into concise summaries.
- Test the subagent and verify context isolation with /context.
Open the subagents interface and create a new agent
Each subagent runs in its own context window with its own system prompt and tool access. Verbose output from the subagent's work stays inside that window. Only the final summary returns to your main session.
- Open the subagents interface by running this command in your Claude Code session:
/agents
- Switch to the Library tab.
- Select Create new agent.
- Choose Project as the scope. This saves the subagent to .claude/agents/ so it's scoped to your current project.
Why project scope instead of personal?
Project-scoped subagents live in .claude/agents/ and can be committed to your repo. Personal subagents go to ~/.claude/agents/ and are available in all your projects. For a test runner tailored to this codebase, project scope is the right fit.
- When prompted for a name, enter test-runner.
- For the description, enter: Runs test suite and reports only failures with context. Use when tests need to run without flooding the main conversation.
- Set the tools to Read, Grep, Glob, Bash. This gives the subagent read access and command execution without write permissions.
- Set the model to haiku.
- Set the effort to low.
Why Haiku with low effort?
The model field supports sonnet, opus, haiku, a full model ID, or inherit. Haiku is fast and lightweight, which is ideal for structured filtering tasks like running tests and parsing output.
The effort frontmatter field overrides the session effort level when the subagent is active. Setting it to low means the subagent uses fewer thinking tokens for this straightforward task.
Write the system prompt
The system prompt is the body of the subagent's markdown file, below the frontmatter. It tells the subagent exactly how to handle the delegated task and what format to return.
- Enter or paste the following system prompt for the subagent:
You are a test execution specialist. When invoked:
1. Run the project test suite using the appropriate test command
2. Capture the full output
3. Filter to only failing tests
4. For each failure, report:
- Test name and file location
- Error message
- Relevant stack trace (first 5 lines only)
- The assertion that failed
Return a concise summary: total tests, passed, failed, and the failure details above.
Do NOT return passing test output. Keep your response under 50 lines.
What does this system prompt do?
- It instructs the subagent to run whatever test command is appropriate for the project (npm test, pytest, go test, etc.).
- It filters the output down to only failures with just enough context to fix them.
- The 50-line cap ensures the summary that returns to your main session is compact.
- Confirm the subagent is created. The /agents interface should now show test-runner in the Library tab.
- Verify the file was created by running:
ls .claude/agents/
- You should see test-runner.md listed in the output.
Subagents created via /agents take effect immediately
If you create or edit a subagent file directly on disk, you need to restart your session to load it. Subagents created through the /agents interface take effect immediately without a restart.
✔️ Awesome, I've got everything!
Great. Double check you see test-runner.md in your .claude/agents/ directory.
ⓧ I'd like to double check the full code
Your .claude/agents/test-runner.md should look exactly like this:
---
name: test-runner
description: Runs test suite and reports only failures with context. Use when tests need to run without flooding the main conversation.
tools: Read, Grep, Glob, Bash
model: haiku
effort: low
---
You are a test execution specialist. When invoked:
1. Run the project test suite using the appropriate test command
2. Capture the full output
3. Filter to only failing tests
4. For each failure, report:
- Test name and file location
- Error message
- Relevant stack trace (first 5 lines only)
- The assertion that failed
Return a concise summary: total tests, passed, failed, and the failure details above.
Do NOT return passing test output. Keep your response under 50 lines.
Test the subagent and verify context savings
The real value of a subagent is measurable. You can verify that verbose output stays isolated by checking your main session's context before and after delegation.
- Before testing, note your current context usage by running:
/context
- Now ask Claude to delegate a test run to your subagent. Type a prompt like:
Use the test-runner agent to run the test suite and report any failures.
What happens during delegation?
Claude spawns the test-runner subagent in a fresh, isolated context window. The subagent runs your tests, processes the full output internally, and returns only the filtered summary. Your main session receives a compact result instead of hundreds of lines of test output.
- After the subagent completes, check your context usage again by running:
/context
- Compare the context grid to your earlier reading. The subagent's verbose test output should not appear in your main session's context at all. Only the concise summary is present.
Claude not delegating to the subagent?
- Make sure you mention the subagent by name in your prompt (e.g., "Use the test-runner agent to..."). Claude uses the description field to decide when to delegate, but explicit naming ensures it.
- Confirm your subagent appears in the Library tab of /agents. If it doesn't, the file may not have been created correctly.
- If you edited the file on disk manually after creating it, restart your session. Only changes made through the /agents interface take effect immediately.
help me troubleshoot why Claude isn't delegating to my test-runner subagent
Your subagent is now handling verbose tasks in isolation. Next up, you'll write a preprocessing hook that filters command output before it even enters context, adding another layer of protection against token bloat.
Write a Preprocessing Hook
Your subagent isolates verbose tasks in a separate context window. But what about commands Claude runs directly in your main session?
Test output, log dumps, and build traces can flood your context with thousands of tokens before you even see them. A PreToolUse hook intercepts commands before execution, letting you pipe their output through filters so only the signal enters your context window.
In this step, get ready to:
- Create a preprocessing hook script that filters test and log output.
- Register the hook in your local settings file.
- Test the hook and verify it intercepts commands via debug logs.
Create the hook script
A PreToolUse hook fires every time Claude is about to use a tool. It receives JSON on stdin describing the tool call, and can return modified input via updatedInput. This lets you rewrite a command before Claude executes it.
- Create the hooks directory by running this command:
mkdir -p .claude/hooks
- Create a file called filter-output.sh inside .claude/hooks/ with the following content:
#!/bin/bash
# Preprocessing hook: filters verbose test/log output before it enters context
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# If running tests, filter to show only failures
if [[ "$cmd" =~ ^(npm\ test|npx\ jest|pytest|go\ test|cargo\ test) ]]; then
filtered_cmd="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:|FAILED|panic:)' | head -100"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered_cmd\"}}}"
elif [[ "$cmd" =~ ^(cat|less|tail).*(log) ]]; then
filtered_cmd="$cmd | grep -E '(ERROR|WARN|FATAL)' | tail -50"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered_cmd\"}}}"
else
echo "{}"
fi
What does this script do?
- input=$(cat) reads the JSON payload that Claude Code passes to every hook via stdin.
- jq -r '.tool_input.command' extracts the shell command Claude is about to run.
- The first if branch matches test runners (npm test, pytest, etc.) and rewrites the command to pipe output through grep for error patterns, capped at 100 lines.
- The elif branch matches log file reads and filters to only ERROR, WARN, and FATAL lines.
- The else branch returns empty JSON, meaning Claude runs the command unchanged.
- The updatedInput field replaces the entire tool input. The permissionDecision of allow means Claude runs it without asking for permission again.
- Make the script executable by running:
chmod +x .claude/hooks/filter-output.sh
- Confirm the file exists and is executable by running:
ls -la .claude/hooks/filter-output.sh
- You should see -rwxr-xr-x (or similar) permissions on the file, confirming it is executable.
Script not marked executable?
If ls shows -rw-r--r-- instead of -rwx, run chmod +x .claude/hooks/filter-output.sh again. Make sure you are running it from the project root directory.
Also confirm jq is installed on your machine. The hook depends on it to parse JSON. Run jq --version to check. On macOS, jq is pre-installed on recent versions.
help me debug my preprocessing hook script
✔️ Awesome, I've got everything!
Great. Double check you have saved filter-output.sh and confirmed it is executable.
ⓧ I'd like to double check the full code
#!/bin/bash
# Preprocessing hook: filters verbose test/log output before it enters context
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
# If running tests, filter to show only failures
if [[ "$cmd" =~ ^(npm\ test|npx\ jest|pytest|go\ test|cargo\ test) ]]; then
filtered_cmd="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:|FAILED|panic:)' | head -100"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered_cmd\"}}}"
elif [[ "$cmd" =~ ^(cat|less|tail).*(log) ]]; then
filtered_cmd="$cmd | grep -E '(ERROR|WARN|FATAL)' | tail -50"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered_cmd\"}}}"
else
echo "{}"
fi
Register the hook in settings
The hook script exists on disk, but Claude Code does not know about it yet. You register hooks in a settings JSON file, mapping them to specific tool events and matchers.
Why settings.local.json instead of settings.json?
Files at .claude/settings.json are project-shared and typically committed to version control. Files at .claude/settings.local.json are personal and not shared.
Since your filtering preferences may differ from teammates, settings.local.json keeps your hook personal.
- Create a file called .claude/settings.local.json in your project root with the following content:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/filter-output.sh",
"args": []
}
]
}
]
}
}
What does this configuration do?
- PreToolUse is the event. The hook fires before any tool executes.
- matcher: "Bash" scopes the hook to fire only when Claude uses the Bash tool. It won't fire for Read, Edit, or other tools.
- ${CLAUDE_PROJECT_DIR} is a placeholder that resolves to your project root at runtime.
- The args: [] field means the hook runs in exec form (no shell tokenization), which is safer for scripts.
- Save .claude/settings.local.json.
- Claude Code watches settings files and hot-reloads them. You do not need to restart your session.
- Run /hooks in your Claude Code session to confirm the hook appears in the list.
Hook not showing in /hooks?
Check that settings.local.json is inside .claude/ (not at the project root). The file path must be .claude/settings.local.json.
Confirm the matcher value is capitalized as Bash (not bash). Matching is case-sensitive, and tool names in Claude Code are capitalized.
help me troubleshoot my Claude Code hook configuration
✔️ Awesome, I've got everything!
Great. Make sure your hook appears when you run /hooks in your Claude Code session.
ⓧ I'd like to double check the full code
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/filter-output.sh",
"args": []
}
]
}
]
}
}
Test the hook with a verbose command
The hook is registered, but you need to confirm it actually fires and rewrites commands. Claude Code supports a --debug-file flag that writes detailed logs to a file, including hook evaluation.
- Open a separate terminal window (outside your current Claude Code session).
- Start a new Claude Code session with debug logging enabled by running:
claude --debug-file /tmp/hooks.log
- In this debug session, ask Claude to run a test command that matches your hook pattern. For example, type a prompt like: "Run npm test for me" or "Run pytest on this project."
- After Claude runs the command, switch to your other terminal and inspect the debug log by running:
grep -i "hook" /tmp/hooks.log | tail -20
- You should see log entries showing the hook was evaluated against the Bash tool call, along with the exit code and output from your script.
What should I see?
The debug log should show that your hook matched the Bash tool invocation, ran filter-output.sh, and returned JSON containing updatedInput. The actual command Claude executed should include the grep and head filters you defined.
If the command did not match your regex patterns (because your project uses a different test runner), the hook returns {} and Claude runs the command unmodified. Adjust the regex in filter-output.sh to match your project's test command.
Hook fires but output isn't filtered?
The regex pattern must match the beginning of the command string. If your test runner is invoked differently (like yarn test or make test), add it to the regex pattern in the if branch.
If the debug log shows no hook evaluation at all, confirm /hooks lists your hook in the debug session. The --debug-file flag does not affect hook loading.
help me debug why my Claude Code hook isn't filtering output
Your preprocessing hook is now intercepting verbose commands before they flood your context. Next up, you will tune effort levels and establish a session architecture for clean context boundaries across tasks.
Dynamic Effort Tuning and Session Architecture
You've built every optimization layer so far: file exclusions, lean instructions, on-demand skills, context-isolating subagents, and preprocessing hooks. The final piece is tuning how Claude thinks per task and how you manage conversation boundaries over time.
Long sessions accumulate context with every message, eventually degrading response quality as the window fills up. Effort levels let you dial reasoning depth up or down per task, and session management commands give you clean boundaries between logical units of work.
In this step, get ready to:
- Configure effort levels for your main session and subagents.
- Establish a session boundary workflow with naming, clearing, and resuming.
- Measure your final context improvement against the Step 1 baseline.
Configure effort levels for different contexts
Effort levels control how much reasoning Claude applies per turn. Lower effort uses fewer tokens and responds faster. Higher effort provides deeper analysis at the cost of more token spend.
What are effort levels?
On Opus 4.7, Claude Code supports five effort levels: low, medium, high, xhigh, and max. Each level trades token spend against capability.
The default on Opus 4.7 is xhigh, which is recommended for most coding and agentic tasks. The effort frontmatter field in subagent definitions overrides the session level when that subagent is active.
- Set your main session effort to xhigh by running this command in your Claude Code session:
/effort xhigh
What does this command do?
This sets the effort level directly to xhigh for your session. The setting persists across sessions, so you only need to run it once. Running /effort with no arguments opens an interactive slider if you want to experiment with different levels later.
- Confirm your test-runner subagent already uses effort: low in its frontmatter. Open .claude/agents/test-runner.md and verify the frontmatter includes:
effort: low
Why different effort levels per subagent?
Your test-runner subagent does structured filtering. It does not need deep reasoning, so low effort keeps it fast and avoids wasting thinking tokens on simple tasks.
For a subagent that reviews architecture or refactors complex code, you would set effort: high in its frontmatter instead. Match the effort level to the cognitive complexity of the task.
Establish a session boundary workflow
Each message in a Claude Code session resends the full conversation history. After many exchanges, accumulated context dominates the window. Clean session boundaries prevent this.
- Practice the session boundary cycle. When you finish a logical unit of work, label it by running:
/rename feature-auth
- Start fresh for your next task by running:
/clear
- Return to a previous session when needed by running:
/resume feature-auth
How does this workflow help context?
The /rename command labels your session so you can find it later. The /clear command starts a new conversation with empty context, while the previous conversation stays available in the /resume picker.
This gives you a fresh context window for each logical task. No stale conversation history from a previous feature leaks into your current work.
Now add custom compaction instructions so Claude preserves what matters when auto-compaction triggers.
- Open your CLAUDE.md file and add this block at the end:
# Compact instructions
When you are using compact, please focus on code changes, test results, and architectural decisions
What does this do?
Claude Code automatically summarizes conversation history when approaching context limits. These instructions tell Claude what to prioritize during that summarization. Without them, Claude might discard important code changes or test outcomes in favour of keeping verbose discussion.
- Save CLAUDE.md.
Compaction instructions not taking effect?
Make sure the heading is exactly # Compact instructions (with the # prefix). Claude Code looks for this specific heading format. Also check that CLAUDE.md is at your project root, not inside a subdirectory.
Why aren't my compact instructions working in CLAUDE.md?
Compare your context footprint to the baseline
Time to measure the impact of everything you've configured. You took a baseline reading in Step 1. Now you can compare.
- Run the context visualizer one final time:
/context
- Compare the colour-coded grid to your Step 1 screenshot. You should see noticeably more green (available context) compared to your starting state.
- Run /usage to see updated token statistics and compare them to your earlier numbers.
What should I see?
Your optimized configuration should show improvements in multiple areas. The .claudeignore prevents Claude from reading irrelevant files. Path-scoped rules only load when relevant. Skills stay hidden until invoked. Your subagent isolates verbose output. The preprocessing hook filters noisy commands.
The combined effect is a leaner, higher-signal context window that gives Claude better information to work with.
- Document your improvement. Note the before and after context utilization so you can reference it later.
Your Claude Code environment is now fully optimized across all layers. Next up, the Secret Mission challenges you to add a dynamic hook that injects project state automatically at session start.
Secret mission
Build a Dynamic Context SessionStart Hook
Every time you start a new Claude Code session, you spend the first message explaining what branch you're on, what you were working on, and what's broken. What if Claude already knew all of that the moment a session began?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely within your local Claude Code configuration. Your Max subscription covers all usage, so there are no ongoing costs from anything you created.
Decide whether to keep your configuration active, disable hooks temporarily, or remove everything entirely.
Resources you used:
- .claudeignore file at your project root.
- .claude/rules/db-rules.md path-scoped rule file.
- .claude/skills/deploy.md on-demand skill file.
- .claude/agents/test-runner.md custom subagent definition.
- .claude/hooks/filter-output.sh preprocessing hook script.
- .claude/hooks/session-context.sh SessionStart hook script.
- .claude/settings.local.json personal hook configuration.
Keep everything running
No action needed. Choose this if you want to continue benefiting from your optimized configuration.
- All hooks, rules, skills, and subagents remain active in your project.
- Your .claudeignore keeps filtering irrelevant files from context.
- The SessionStart hook continues injecting project state at every new session.
Pause - I'll come back to this later
Disable active hooks without removing your files. This stops the hooks from running while preserving your configuration for later.
- Rename your settings file to disable hooks temporarily by running:
mv .claude/settings.local.json .claude/settings.local.json.bak
- To re-enable later, rename it back:
mv .claude/settings.local.json.bak .claude/settings.local.json
- Your .claudeignore, rules, skills, and subagent files remain on disk and will resume working once you restore the settings file.
Delete - I don't want to use this again
Remove all project resources created during this project. This deletes every configuration file you built.
- Delete the .claudeignore file by running:
rm .claudeignore
- Delete the hooks, rules, skills, agents, and settings by running:
rm -rf .claude/hooks/ .claude/rules/ .claude/skills/ .claude/agents/ .claude/settings.local.json
- If you have no other files in the .claude/ directory, remove it entirely:
rmdir .claude 2>/dev/null || echo "Other files remain in .claude/"
- If you trimmed your CLAUDE.md during this project and want to restore it, use git checkout CLAUDE.md to revert to the previous version.
Nice Work!
Nice Work!
You did it! Your Claude Code environment is now systematically optimized for context efficiency, giving you better responses and fewer forgotten instructions across long sessions.
You've learned how to:
- Configure .claudeignore, a lean CLAUDE.md, and path-scoped rules with on-demand skills that keep your base context minimal and load domain knowledge only when needed.
- Build a custom subagent that isolates verbose tasks in its own context window, returning only concise summaries to your main session.
- Create a preprocessing hook that filters noisy command output before it enters your context, combined with effort levels and a session architecture workflow for clean context boundaries.
- Secret Mission: Build a SessionStart hook that automatically injects your current branch, recent commits, and project state into Claude's context at the start of every session.
Ready to quiz yourself?