Chapter 2. Installing Claude Code and Configuring It for Real Work
Install Claude Code with the native script
(curl -fsSL https://claude.ai/install.sh | bash on macOS,
Linux, and WSL), or through Homebrew, WinGet, npm, or a signed Linux
repository. Configure it by hand-editing JSON settings files, because
claude config get and claude config set, still
circulating in older writeups, do not exist in 2.1.211: the word
“config” parses as a prompt, not a command.
The machine this book runs on carries Claude Code 2.1.211 as a native
build, and every path, key name, and byte count in this chapter is read
off its disk rather than quoted from documentation. Where the docs and
the binary disagree, the binary wins. On this build,
claude config --help produces output byte-identical to
claude zzzbogusnotacommand --help. Configuration lives in
JSON files you edit by hand, plus the in-session /config
command and the --settings and
--setting-sources flags. Treat any tutorial built on
claude config set as archaeology.
Install routes and where the binary lands
The first-party install routes:
| Route | Command | Updates |
|---|---|---|
| Native script, macOS/Linux/WSL | curl -fsSL https://claude.ai/install.sh \| bash |
auto-updates in the background |
| Native script, Windows PowerShell | irm https://claude.ai/install.ps1 \| iex |
auto-updates in the background |
| Homebrew stable | claude-code cask |
manual brew upgrade; about a week behind |
| Homebrew latest | claude-code@latest cask |
manual brew upgrade |
| WinGet | winget install Anthropic.ClaudeCode |
manual winget upgrade |
| npm | npm install -g @anthropic-ai/claude-code |
needs Node 22 or later as of v2.1.198 |
Linux also gets signed apt, dnf, and apk repositories at
https://downloads.claude.ai/claude-code/{apt,rpm,apk}/<channel>
with stable and latest channels; the package
name is claude-code. Even the npm route is a wrapper around
the same native binary now: the package pulls it in through per-platform
optional dependencies like
@anthropic-ai/claude-code-darwin-arm64.
What the native installer leaves on disk is compact.
which claude resolves to ~/.local/bin/claude,
a symlink into ~/.local/share/claude/versions/2.1.211. The
target is one Mach-O 64-bit executable of 251,966,736 bytes, not a node
wrapper script, and ~/.claude.json records
installMethod: "native". One consequence of the packaging:
strings -a over the 252 MB binary returns zero matches for
the help text it prints at runtime. The payload is compressed, so you
cannot enumerate a build’s command surface by grepping the binary. If
you want to know what a given build can do, you ask the build.
Updates are governed by one setting and two environment variables.
autoUpdatesChannel accepts "latest" (the
default) or "stable". DISABLE_AUTOUPDATER
stops the background update check; DISABLE_UPDATES blocks
manual updates too. This machine sets DISABLE_AUTOUPDATER,
because a shared box running concurrent agent sessions does not need the
CLI swapping itself out mid-task; updates happen when I run
claude update. The receipt for the last one sits at
~/.claude/.last-update-result.json with the shape
{timestamp, path, outcome, status, version_from, version_to, error_code}:
path "native", outcome and status "success",
2.1.207 to 2.1.211, error_code null.
claude doctor verifies all of it. On this machine it
reports: Running native (2.1.211), Commit 17a4b6d7b2ee, Platform
darwin-x64, Config install method native, Search OK (bundled),
Auto-updates disabled (set by env: DISABLE_AUTOUPDATER), Auto-update
channel latest, Last update attempt success to 2.1.211 (2026-07-16),
plus a Remote Control section and a final installation-issues verdict.
Its help text notes it reads settings files in the current directory
without a trust prompt. Run it after any settings edit; the failure mode
it catches is described below.
Settings precedence: which file wins
Settings resolve across layers, highest precedence first:
- Managed settings (fleet policy; cannot be overridden, not even by command-line arguments)
- Command-line arguments
- Local project settings,
.claude/settings.local.json - Shared project settings,
.claude/settings.json - User settings,
~/.claude/settings.json
Inside the managed tier there is no merging: one source wins, in the
order policyHelper output, remote server-managed settings, MDM/OS-level
policy, the file pair managed-settings.json plus a
managed-settings.d/ drop-in directory, then the Windows
HKCU registry. On macOS the file path is
/Library/Application Support/ClaudeCode/managed-settings.json.
This machine has none; there is no fleet policy above a solo operator.
The drop-in directory follows the systemd convention: the base file
merges first, then *.json fragments in alphabetical order,
scalars overriding, arrays concatenating with duplicates removed,
objects deep-merging.
Across the non-managed tiers, scalar keys resolve by precedence, but
array-valued keys like permissions.allow and
sandbox.filesystem.allowWrite concatenate and de-duplicate
across scopes. That cuts both ways. A project can extend my user
allowlist without restating it, and nothing at a lower tier can subtract
a rule granted above it. Subtraction is what
permissions.deny is for: rules evaluate deny first, then
ask, then allow, and the first match decides regardless of specificity.
When you need to control which tiers load at all,
--setting-sources takes a comma-separated list whose three
valid values are user, project, and
local.
Two operational facts matter more than the precedence chart. First,
validation for user, project, and local files is strict and
file-granular: one bad key rejects the whole file. An interactive
session shows a Settings Error dialog at startup. A -p
(print-mode) run does not: the CLI’s own help for -p
describes invalid settings files as ignored with no error dialog, while
the settings docs say a summary prints to stderr. Either way, a cron job
or CI script proceeds without the permission rules you thought you set.
This is why claude doctor follows any hand edit (it lists
each invalid entry with its source and field), why /status
shows a Setting sources line where a broken file goes missing, and why a
$schema line pointing at
https://json.schemastore.org/claude-code-settings.json is
worth adding so your editor catches the typo before the harness discards
the file. Second, settings hot-reload mid-session, including
permissions, hooks, and
apiKeyHelper; only model and
outputStyle are read once at start.
The documented key table runs to 124 keys. My user file uses nine of them.
What is in
~/.claude/settings.json on this machine
The keys present in ~/.claude/settings.json on this
machine, with real values and the longer lists elided:
{
"permissions": {
"allow": [
"Bash(gh auth status)",
"Bash(gh auth status:*)",
"Bash(git commit -m ' *)"
]
},
"hooks": {},
"enabledPlugins": {
"superpowers@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true,
"vercel@claude-plugins-official": true,
"suede-skills@suede": true
},
"skipWorkflowUsageWarning": true,
"theme": "dark",
"agentPushNotifEnabled": true,
"inputNeededNotifEnabled": true,
"promptSuggestionEnabled": true,
"emojiCompletionEnabled": true
}The permission grammar deserves a close read, because this one file
demonstrates three things the docs state in the abstract. Rules are
strings of the form Tool or Tool(specifier).
Bash rules carry the command inside the parens, and two distinct
wildcard forms appear side by side: the colon-star suffix
(Bash(gh auth status:*)) and a bare trailing space-star
(Bash(git commit -m ' *)). The same command shows up in
both exact and wildcard form, Bash(gh auth status) beside
Bash(gh auth status:*), because the exact form does not
imply the argument-wildcard form; approving the bare command did not
cover invocations with flags, so both rules accumulated. Non-Bash tools
are named bare (WebSearch), and MCP tools use the
mcp__<server>__<tool> form
(mcp__scheduled-tasks__list_scheduled_tasks). This machine
runs on permissions.allow alone; deny and
ask are absent.
enabledPlugins holds 12 entries: 4 true and 8 set to
false rather than deleted. Disabling keeps a plugin installed and
listed; removal is a separate act. And note what is not in this file.
The suede marketplace that suede-skills@suede
resolves against is registered in
~/.claude/plugins/known_marketplaces.json, not here, so at
user level the two halves of plugin registration live in different
files. Five marketplaces are registered on this machine:
claude-plugins-official, superpowers-dev,
google-labs-code-stitch-skills, suede, and addy-agent-skills.
The absent keys say as much as the present ones. No env,
no model, no statusLine, no
outputStyle, no apiKeyHelper, no
cleanupPeriodDays: the defaults hold, and nothing
secret-shaped goes near a settings file. hooks is present
but {} at this tier; the working hook lives one file
over.
settings.local.json: hooks wired to one machine
Beside the user file sits ~/.claude/settings.local.json
with three top-level keys: permissions, hooks,
and a description string. That last one is not a documented
settings key; the validator tolerates it, and it works as an annotation
telling a future reader why the file exists. The one live hook on this
machine is a PostToolUse hook, and its shape is the whole anatomy of the
hook system:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\"",
"timeout": 5,
"statusMessage": "..."
}
]
}
]
}
}Each event key holds an array of matcher objects. The matcher is one
pipe-delimited string of tool names, not an array. Each matcher object
holds an inner hooks array whose entries carry
type: "command", the command line, a timeout
in seconds (5 here), and a statusMessage, a short
present-participle label the UI shows while the hook runs.
${CLAUDE_PROJECT_DIR} interpolates the project directory,
which is what lets a skill ship its own hook script (the target here is
a 1,999-byte hook.mjs inside the impeccable skill’s
scripts/ directory) and have the harness run it after
edits. A supporting detail: ~/.claude/package.json contains
{"type":"commonjs"} and nothing else, pinning the module
system for any .js hook script under
~/.claude.
Why does this live in the local file? Because the command embeds an
assumption about this machine’s layout, and because hooks are the config
most prone to rot. The evidence sits on the same disk.
~/.claude/hooks/ exists and is empty, zero entries, while a
retained backup, settings.json.bak-20260618-223239,
references six scripts by that path. The backup documents a fuller
configuration that once ran here: SessionStart,
PreToolUse, and PostToolUse arrays plus a
top-level statusLine of shape
{type: "command", command: "..."}. It preserves two shape
facts worth stealing. SessionStart entries carry no matcher
key at all. And one event array can hold multiple objects with the same
matcher: the backup has three PreToolUse entries all matching
Write|Edit, each running a different script, instead of one
entry with three inner hooks. That full stack got cut down to a single
hook because the scripts moved and the config kept pointing at ghosts.
Hooks that reference machine-specific paths belong in the untracked
local file, where the blast radius of a stale path is one machine.
.claude/settings.json:
what rides with the repo
A committed .claude/settings.json is for configuration a
fresh clone needs before an agent on some other machine can behave the
way mine does. The cleanest live example here is
~/code/podcast-sync/.claude/settings.json, which holds two
keys and nothing else:
{
"extraKnownMarketplaces": {
"suede": {
"source": { "source": "github", "repo": "JasonColapietro/suede-creator-skills" }
}
},
"enabledPlugins": { "suede-skills@suede": true }
}Compare this with the user tier, where marketplace registration and plugin enablement live in different files. At project level the two halves combine in one committed file, which is the point: clone the repo anywhere and the Suede skills marketplace resolves and its plugin enables with no per-machine setup.
My working memory records a 2026-07-21 sweep that claimed to put this
file into every public repo of mine. A 2026-07-29 remote-tree check
disproved that claim: podcast-sync carries
.claude/settings.json on origin/main;
strumly and suede-agent-studio do not. The
sweep was incomplete, and the memory entry overstated it. The pattern is
proven in one repository and remains a rollout standard for the others,
not a completed estate-wide fact.
The fourth file, a repo’s own
.claude/settings.local.json, takes what the committed
project file must not: per-checkout permission grants, experiments,
anything with an absolute path in it.
launch.json: dev servers as declared config
~/.claude/launch.json is not a settings file and plays
no precedence game. It declares dev servers so the harness can start
them by name instead of me pasting npm run dev incantations
into prompts. Two top-level keys: version (the string
"0.0.1") and configurations, an array holding
41 entries on this machine. The union of fields across all 41 is six
names: name, runtimeExecutable,
runtimeArgs, cwd, port,
autoPort. A real entry:
{
"version": "0.0.1",
"configurations": [
{
"name": "suede-agent-studio",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--", "-p", "3210"],
"cwd": "/Users/jasoncolapietro/code/suede-agent-studio",
"port": 3210
}
]
}Field discipline, learned across 41 entries. runtimeArgs
is a flat array of separate string tokens: the npm double-dash separator
and the port flag are individual elements ("--",
"-p", "3210"), never one combined string.
runtimeExecutable takes a bare command resolved on PATH
(npm appears 29 times, python3 10 times, node once) or an absolute path
to a project-local binary; one entry points at
/Users/jasoncolapietro/code/strumly/node_modules/.bin/next.
cwd is optional: 35 of 41 entries set it, and the 6 that
skip it pass the directory inside runtimeArgs instead, the
pattern being python3 -m http.server with
--directory <absolute path>.
autoPort: true appears on 3 entries, all with ports that
could collide (4173, 4180, 3001); everywhere else I want the fixed port,
because a server that moves ports breaks the URLs an agent has already
written down.
name is the unique handle used to start the server, and
the naming convention carries load: names encode repo plus task or
worktree (suede-muse-seo-sweep,
agent-studio-visual-baseline), because concurrent sessions
in sibling worktrees each need their own server on their own port, and a
name like dev works for one session and no more.
The companion convention is the port table. launch.json says how to start a server; the project CLAUDE.md carries a table mapping each app to its port and working directory, so a session knows before it starts anything that strumly’s dev server belongs on 3001 and suede-agent-studio’s on 3210, and that a worktree gets its own row rather than borrowing the main checkout’s port. One app, one port, written down where the agent reads it at session start. That single convention has done more to stop two agents from fighting over the same port than any amount of prompt discipline.
Putting ~/.claude under
git
~/.claude is a git repository on branch
main with remote
https://github.com/JasonColapietro/claude-dotfiles.git. Its
.gitignore is allowlist-style: a bare *
ignores everything, then negations un-ignore /.gitignore,
/CLAUDE.md, /launch.json,
/settings.json, /package.json,
/agents/ with /agents/**, and
/hooks/ with /hooks/**.
settings.local.json is not in the allowlist and stays
untracked. Read that closely: the harness does not enforce the
local/shared split; my gitignore does. The tier system tells the CLI
which file wins, but which file leaves the machine is a decision git
makes.
Two adjacent details round out the layout.
~/.claude/skills/ is its own independent repository (remote
suede-personal-skills), and
~/.claude/commands/ is a repository with no remote
configured; both are excluded from the parent dotfiles repo to avoid
nested-repo gitlinks. File modes differ too: settings.json
and settings.local.json sit at mode 600,
launch.json at 644.
The tier logic, compressed to a placement rule:
| Tier | File | What goes in it |
|---|---|---|
| Managed | /Library/Application Support/ClaudeCode/managed-settings.json |
Fleet policy. Absent on a solo machine. |
| User | ~/.claude/settings.json |
Preferences and broad allow rules that follow you to every repo. Tracked in dotfiles. |
| Project | <repo>/.claude/settings.json |
Registration a fresh clone needs: marketplaces, plugin enablement. Committed. |
| Local | .claude/settings.local.json |
Machine-wired hooks, per-checkout grants, absolute paths. Untracked. |
If a value would break on another machine, it goes local. If a clone without it behaves differently, it goes project. If it is about you rather than any repo, it goes user. And if someone above you sets it, it is managed and you were never asked, which is the one tier a solo operator gets to skip.