Claude Code Hidden Config: What the Docs Don't Tell You
A post on buildingbetter.tech hit nearly 200 upvotes on Hacker News with a simple premise: read the Claude Code source code (the author pinned it to @anthropic-ai/claude-code@2.1.87) and write down the config that Anthropic didn’t document.
This post is the reference that teardown deserved — the real undocumented hook fields, settings.json keys, and agent frontmatter, with working examples.
Fair warning: undocumented means unstable. Everything here was traced from one version of the source by one developer. Treat it as “works now, re-verify after upgrades” — and where this post could only confirm a field’s existence, not its full behavior, it says so.
What you need
Claude Code installed, and a .claude/ directory (either ~/.claude/ for personal settings or .claude/ in a project for git-shareable ones). The config below lives in settings.json, skill files, and agent files — not in CLAUDE.md, which is a different thing (your project context).
The hooks you didn’t know you could return from
Claude Code’s hook system is documented at the surface — you can wire a command to an event. What the teardown surfaced is the structured JSON a hook can write to stdout to change what Claude does next. Per the article, four events accept return values:
PreToolUse— fires before a tool runs. Return:updatedInput(rewrite the tool’s input mid-flight),permissionDecision(forceallow/denywithout prompting),permissionDecisionReason(shown in the UI),additionalContext(inject text into the conversation).SessionStart— fires when a session opens. Return:watchPaths(set up file watching that triggersFileChangedevents),initialUserMessage(prepend content to the first user message),additionalContext(inject context for the whole session).PostToolUse— fires after a tool returns. Return:updatedMCPToolOutput(modify what Claude sees from an MCP tool response),additionalContext.PermissionRequest— return adecisionwithupdatedInputorupdatedPermissions.
The most useful one to reach for first is PreToolUse with permissionDecision — it’s how you auto-approve a known-safe command pattern without a flag, and the permissionDecisionReason shows up in the UI so future-you remembers why.
Three hook flags that change timing
Beyond the documented hook fields (type, command, matcher, timeout, if, statusMessage), the teardown found three flags that control when and how often a hook runs:
// .claude/settings.json — inside a hook definition
{
"type": "command",
"command": "./hooks/notify.sh",
"once": true, // fires exactly once, then auto-removes itself
"async": true, // runs in the background without blocking Claude
"asyncRewake": true // runs in the background, but exit code 2 wakes the
// model back up and blocks the operation
}
once is the quietly useful one — a hook that should run a single time per session (a one-shot setup check, a “you’re on the prod branch” warning) instead of firing on every matching event.
settings.json: the keys that aren’t in the docs
These live in ~/.claude/settings.json (personal) or .claude/settings.json (project, git-shareable):
{
"hooks": { /* event-name → hook definitions, as above */ },
"autoMode": {
"allow": [ /* permission patterns to auto-approve */ ],
"soft_deny": [ /* patterns that always require confirmation */ ],
"environment": "This is a local dev machine with no production database access"
},
"autoMemoryEnabled": true,
"autoDreamEnabled": true,
"permissions": {
"allow": [ /* glob patterns */ ],
"deny": [ /* glob patterns */ ],
"ask": [ /* glob patterns */ ]
}
}
What each does, per the teardown:
autoMode— an auto-approval layer.allowpatterns get auto-approved;soft_denypatterns always require confirmation;environmentis a plain-English string the classifier reads to understand your setup. (The article notes the classifier’s internal name in the source isyoloClassifier.ts— a nice tell for how Anthropic thinks about it.)autoMemoryEnabled— automatically extracts durable memories from your sessions.autoDreamEnabled— activates background “dream” consolidation: every 24 hours, if 5+ sessions have accumulated, a background agent reviews past transcripts and consolidates memories.permissions— the familiarallow/deny/askglob arrays.
autoMode.environment is the sleeper here. A sentence like the one above gives the auto-approval classifier real context instead of you maintaining a brittle list of glob patterns.
Skill and agent frontmatter
The teardown also found undocumented frontmatter fields. In a skill (.claude/skills/<name>/SKILL.md):
---
name: my-skill
description: ...
model: haiku # override the model — haiku/opus/inherit
effort: high # how hard the model thinks — low/medium/high/max
disable-model-invocation: true # only explicit /my-skill works, no auto-invoke
shell: bash # which shell executes the skill
context: fork # run as a background forked subagent
---
One real gotcha the article calls out: on a forked skill (context: fork), setting a different model breaks the cache — either omit model or use model: inherit.
In an agent (.claude/agents/<name>.md):
---
name: reviewer
color: purple # red/orange/yellow/green/blue/purple/pink/gray
memory: project # persistent memory — user/project/local
omitClaudeMd: true # skip loading the CLAUDE.md hierarchy
requiredMcpServers: [...] # name patterns; agent hides if these MCP servers aren't configured
effort: high
---
There’s one field worth singling out: criticalSystemReminder_EXPERIMENTAL, a short message re-injected as a system reminder every turn — it survives even conversation compaction. The article is blunt about it: EXPERIMENTAL is in the actual field name in the source, so Anthropic’s own engineers consider it unstable. Useful, but don’t build anything load-bearing on it.
The Python hook library
Writing hook stdin/stdout plumbing by hand is tedious. A community package, claude-hook-utils, wraps it in a typed class interface:
# pip install claude-hook-utils
from claude_hook_utils import HookHandler, PreToolUseInput, PreToolUseResponse
class DataClassValidator(HookHandler):
def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
if not input.file_path_matches('**/app/Data/**/*.php'):
return None
if input.content and '#[TypeScript()]' not in input.content:
return PreToolUseResponse.deny(
"Data classes must have #[TypeScript()] annotation"
)
return PreToolUseResponse.allow()
if __name__ == "__main__":
DataClassValidator().run()
You subclass HookHandler and override the event you care about — pre_tool_use, post_tool_use, user_prompt_submit, or session_start — then call .run(). The PreToolUseInput gives you typed fields (tool_name, tool_input, file_path, content, command, and helpers like file_path_matches(*globs)), and PreToolUseResponse exposes .allow(), .deny(reason), and .ask(reason) so you’re not hand-building the JSON the runtime expects.
Where this breaks
Version drift. None of this is in the official docs, so there’s no deprecation promise. It was read from claude-code@2.1.87; after an upgrade, a field may be renamed or silently ignored. Keep a comment next to each undocumented key noting where it came from, so you know what to re-check.
One reader, one version. This is a single developer’s reading of the source — the strongest signal you can get short of official docs, but not the same thing. The criticalSystemReminder_EXPERIMENTAL field is the explicit warning: Anthropic flagged it unstable in the name itself.
Next steps
The buildingbetter.tech teardown is the primary source — worth reading in full if you want to go deeper than this reference. For hooks specifically, claude-hook-utils is the cleanest path to a working hook without reverse-engineering the stdin/stdout contract yourself. Start with one PreToolUse hook that returns a permissionDecision; once you’ve seen a hook change Claude’s behavior, the rest of the surface is easy to evaluate.
← Back to blog