Skip to main content
AcademytutorialWorkstation Setup tutorial series — Part 3: Install and configure Claude Code

Workstation Setup tutorial series — Part 3: Install and configure Claude Code

Claude Code in VS Code, sign-in, and the mandatory global settings and safety hooks that intercept destructive shell commands before they run. Short but important — running Claude Code without these hooks is how people accidentally wipe their own repo. Third of six short modules.

TutorialWorkstationClaude CodeHooksSettingsSafetyTutorial series
14 min read

With the runtimes in place from Part 2, it's time to add the AI pair programmer we actually work with: Claude Code. This part covers the install, the sign-in, and — the bit you should not skip — the mandatory global settings and safety hooks that keep Claude from running destructive shell commands without your approval. Short part, important part.

Step 1: install the Claude Code extension

Inside your WSL-connected VS Code window:

  1. Ctrl+Shift+X to open Extensions.
  2. Search for Claude Code by Anthropic.
  3. Click Install.

Or from the WSL terminal:

code --install-extension anthropic.claude-code

You should already have this from Part 2 (it was in the extension list). If so, just verify it shows up in the sidebar.

Step 2: sign in

Open the Claude Code panel from the VS Code sidebar (the small Claude icon). Click Sign in and follow the OAuth flow in your browser. Once signed in, the panel shows your subscription tier in the bottom-right.

Working from the terminal? Run claude auth login instead. This is needed if you want to use the claude CLI for skills like /opsx-apply-loop that run inside Docker containers. Optional for now; come back to it once you're comfortable.

Try a quick test prompt: ask Claude something simple like "what's the current directory?" and confirm you get an answer. If you see a permission prompt for a Bash command — that's the first sign that the safety hooks aren't installed yet. Don't approve anything destructive; finish the next steps first.

Step 3: clone the .github repo

The global settings live in the canonical Conduction/.github repo on Codeberg. Clone it once:

mkdir -p ~/code/conduction
cd ~/code/conduction
git clone [email protected]:Conduction/.github.git
cd .github

No SSH key yet? Part 2 — Codeberg auth walks through it. As a fallback you can also clone over HTTPS: git clone https://codeberg.org/Conduction/.github.git — that works without auth but won't let you push.

You only need to clone this once — the version-check hook fetches updates via the GitHub API after this (the settings repo is mirrored from Codeberg to ConductionNL/.github on GitHub for the hook), no further git pull needed.

Step 4: install the global settings + hooks

From inside the cloned .github repo:

REPO_ROOT="$(pwd)"

mkdir -p ~/.claude/hooks

cp "$REPO_ROOT/global-settings/settings.json" ~/.claude/settings.json
cp "$REPO_ROOT/global-settings/block-write-commands.sh" ~/.claude/hooks/block-write-commands.sh
cp "$REPO_ROOT/global-settings/block-config-tool-writes.sh" ~/.claude/hooks/block-config-tool-writes.sh
cp "$REPO_ROOT/global-settings/check-settings-version.sh" ~/.claude/hooks/check-settings-version.sh
chmod +x ~/.claude/hooks/*.sh

cp "$REPO_ROOT/global-settings/VERSION" ~/.claude/settings-version
echo "$REPO_ROOT" > ~/.claude/settings-repo-path

# Online version checking via GitHub API (recommended — no local repo required):
cp "$REPO_ROOT/global-settings/settings-repo-url.example" ~/.claude/settings-repo-url

That's the working set. What did you just install?

FileWhat it does
~/.claude/settings.jsonThe user-level permissions allowlist — which commands Claude can run without asking, which need approval, which are blocked. Plus references to the two hooks below.
~/.claude/hooks/block-write-commands.shThe big one. Runs before every Bash tool call. Detects write/destructive commands (rm, mv, git push --force, chmod, etc.) and prompts for explicit approval.
~/.claude/hooks/block-config-tool-writes.shRuns before every Write/Edit/MultiEdit call. Refuses any tool that tries to write to ~/.claude/ or produce a script that would. Protects the hooks themselves from being silently rewritten.
~/.claude/hooks/check-settings-version.shRuns on session start. Compares your installed version against ConductionNL/.github and warns you if you're behind.
~/.claude/settings-repo-urlThe repo slug the version check uses to fetch the canonical version (ConductionNL/.github).

Tracking a non-default branch? Copy settings-repo-ref.example to ~/.claude/settings-repo-ref and put your branch/tag/SHA in it. For most developers the default — main — is right.

Step 5: lock the hooks with chattr +i

This is the layer no Claude command can bypass. Even if every other guard fails — a hook gets disabled, a permission gets approved by accident — the kernel refuses the write:

sudo chattr +i ~/.claude/settings.json ~/.claude/hooks/*.sh ~/.claude/settings-version

chattr +i sets the immutable bit on Linux file systems. After this, only sudo chattr -i (which Claude can't run without prompting you) can clear it.

chattr not supported? The Conduction setup assumes WSL2 on the standard ext4 file system, which supports chattr +i. If your environment doesn't (some non-ext4 file systems return Operation not supported), skip this step — the hook layers from Step 4 still defend in depth. The kernel layer is the strongest of the four, so if you can run chattr, do.

Step 6: restart Claude Code

The hooks register at session start. Restart Claude Code — Ctrl+Shift+P"Developer: Reload Window" in VS Code is a quick way — and start a new session.

On the next session start, Claude's first message should relay something like:

New session started — Global Claude Settings checked. Settings are up to date (v1.7.0).

If you instead see UPDATE REQUIRED, your installed version is behind main. To update:

sudo chattr -i ~/.claude/settings.json ~/.claude/hooks/*.sh ~/.claude/settings-version

Then ask Claude "update my global settings to <version>" — it pulls the new files from GitHub. After that, re-lock:

sudo chattr +i ~/.claude/settings.json ~/.claude/hooks/*.sh ~/.claude/settings-version

What's the difference between global and project settings?

A common point of confusion. Two layers:

LayerLives atCoversWho maintains it
Global (user-level)~/.claude/settings.json + ~/.claude/hooks/Safety policy on your machine. Read-first, write-with-approval. Affects every project you open.You — but from the canonical Conduction templates. Versioned in Conduction/.github on Codeberg (mirrored to ConductionNL/.github on GitHub for the version-check hook).
Project-level.claude/settings.json inside each repoProject-specific allowlist — which MCP servers to enable, which Bash commands are pre-approved for this project's tests.The project maintainers; checked into the repo.

The two complement each other. Project settings can pre-approve specific commands for that project's tests; they cannot loosen the global write-approval policy. If a project tries to allow rm -rf without a prompt, the global hook still catches it.

The default Conduction install ships three hooks. In one sentence each:

  • block-write-commands.sh — every Bash command Claude wants to run gets checked first. Destructive or write-class commands (rm, mv, chmod, git push --force, npm publish, etc.) trigger an approval prompt; safe read-only commands pass through silently.
  • block-config-tool-writes.sh — every Write/Edit/MultiEdit Claude attempts gets checked first. If the target is inside ~/.claude/, or the content would create a script that writes to ~/.claude/, the hook refuses. This is what stops Claude from quietly disabling its own safety net.
  • check-settings-version.sh — runs once at session start. Compares your installed version to the canonical repo and tells you (in the chat) whether you're up to date.

The first two are the safety net. The third is the heartbeat that keeps that safety net current.

Troubleshooting

Session-start panel says 'CONFIGURATION ERROR'

Read the error line directly below the panel. Most common causes: gh not authenticated (run gh auth login), jq not installed (sudo apt install jq), or ~/.claude/settings-repo-url missing (recopy from global-settings/settings-repo-url.example).

chattr: Operation not supported

Your file system may not support the immutable attribute (ext4 inside WSL2 does; some non-ext4 file systems don't). Skip the immutable-lock step — the hook layer from Step 4 still defends in depth.

Claude offered to run rm -rf and no approval prompt appeared

The hooks didn't load. Confirm the files exist (ls -la ~/.claude/hooks/), are executable (chmod +x), and Claude Code was restarted after install. If still nothing, re-run Step 4 — odds are settings.json didn't get copied in.

Session-start line mentions a new session but no version number is shown

The version file is missing. Run cat ~/.claude/settings-version — if empty, recopy from global-settings/VERSION in the cloned repo.

Test yourself

Five short questions to check that this part landed. Stuck? Click Hint. Curious about the answer? Click Answer.

1. What is the difference between global and project-level Claude settings, and why do we need both?

Hint

Where each lives, what each controls, and how they relate when both are present.

Answer

Global settings live at ~/.claude/settings.json + ~/.claude/hooks/. They define the safety policy on your machine and apply to every project you open. The Conduction global settings (mandatory) enforce read-first, write-with-approval behaviour.

Project settings live at .claude/settings.json inside each repo. They define project-specific allowances: which MCP servers to enable, which test commands are pre-approved, which file paths are read-only for that project.

They complement each other. Project settings can pre-approve commands that the global policy also allows; they cannot loosen the global write-approval policy. The global hook always runs first.

2. The Conduction global-settings install ships two enforcement hooks, not one: block-write-commands.sh and block-config-tool-writes.sh. Why both? What does each catch that the other misses?

Hint

The two hooks operate on different Claude tool calls. One guards the Bash tool; the other guards the file-write tools. Think about what each tool can and can't do.

Answer
  • block-write-commands.sh intercepts every Bash call. It catches Claude trying to run rm -rf, git push --force, chmod, npm publish, etc. — anything destructive that goes through a shell.
  • block-config-tool-writes.sh intercepts every Write/Edit/MultiEdit call. It catches Claude trying to write directly to ~/.claude/ (e.g. disabling its own hooks) or producing a script that would later do the same.

What each catches that the other misses: a Bash command can rm files but can't make a Write tool call. A Write tool can edit ~/.claude/settings.json directly without ever running a shell command. If only the Bash hook existed, Claude could rewrite the safety net without invoking Bash. If only the Write hook existed, Claude could rm -rf your repo without editing a file. Both are needed because Claude has two separate paths to a destructive action.

3. Why does Step 5 use chattr +i on top of the hooks, and what's the trade-off?

Hint

Defense in depth — what does the kernel guarantee that an in-process hook cannot?

Answer

chattr +i sets the immutable bit at the kernel level. Once set, no process — including any Claude command — can write to those files until sudo chattr -i clears it. That's the one layer no in-process hook can bypass.

The trade-off: when a legitimate update is needed, you have to sudo chattr -i first, then re-lock with sudo chattr +i afterwards. A small friction tax in exchange for a guarantee that the safety net itself can't be silently rewritten.

4. You've just installed the global hooks and restarted Claude Code. What's the fastest way to confirm the Bash hook is actually intercepting — without sitting around waiting for Claude to spontaneously suggest something destructive?

Hint

Two things to look for at session start, and one thing you can prompt Claude to do that's harmless but will trip the hook.

Answer

Two confirmations:

  1. Session-start message. Claude's first message should include "Global Claude Settings checked. Settings are up to date". If that line is missing, the version-check hook isn't loaded — meaning the write-blocker is probably also missing.
  2. Provoke an approval prompt deliberately. Ask Claude something innocuous that touches a write-class command, e.g. "check whether rm is on my PATH" — the hook should intercept the Bash call and ask for approval. Decline the prompt; you just wanted to see the interception fire.

If the prompt doesn't appear, the hook didn't load — go back to Step 4 and confirm ~/.claude/settings.json was actually copied and references the hooks correctly.

5. Claude offers to run rm -rf ./node_modules and the command runs immediately — no approval prompt appears. What does that tell you, and what do you check first?

Hint

The hook layer is supposed to catch exactly this. If it didn't, one of three things is true — and only one of them is "everything's fine".

Answer

It tells you the Bash hook didn't run. Three possibilities, in order of likelihood:

  1. The hook isn't installed. Run ls -la ~/.claude/hooks/ and confirm block-write-commands.sh exists and is executable (+x). If missing, re-run Step 4.
  2. ~/.claude/settings.json doesn't reference the hook. The hook only runs if settings.json registers it. Open the file and confirm the PreToolUse block points at block-write-commands.sh.
  3. Claude Code was started before the hooks were installed. Hooks load at session start. Reload the VS Code window (Ctrl+Shift+P → "Developer: Reload Window") and re-try.

Until one of those three is fixed, you're effectively running Claude with no safety net. Don't dismiss this as a one-off — investigate before continuing.

Next step

Claude Code is wired up and the safety net is in place. Time to give Claude direct access to the Conduction systems via the MCP server.