Control Claude Code with hooks and skill settings
Add Claude Code hooks that turn risky pushes into dry runs, load git context and run tests in the background, then give a skill its own model.

What you need
On Windows, hooks run in Git Bash. Without Git Bash they run in PowerShell, and these scripts need rewriting.
Step 1: Write a script that rewrites a command
5 minA PreToolUse hook sees each tool call before it runs, and it can change the call's input.
- In your project, run
mkdir -p .claude/hooks. - Save the script below as
.claude/hooks/dry-run-push.sh. - Run
chmod +x .claude/hooks/dry-run-push.shso it can run. - Test it:
echo '{"tool_input":{"command":"git push"}}' | .claude/hooks/dry-run-push.sh.
Your script#!/bin/bash # Turns any git push into a dry run before it runs. jq '{hookSpecificOutput: {hookEventName: "PreToolUse", updatedInput: (.tool_input + {command: (.tool_input.command | sub("git push"; "git push --dry-run"))})}}'You should see JSON with
git push --dry-runas the new command.It says
jq: command not found?Install jq with your system's package manager, then run the test again.
updatedInputreplaces the whole input, so the script keeps the other fields and changes onlycommand.- In your project, run
Step 2: Register the hook
4 minThe settings file you pick decides who gets the hook: your team, just you, or all your projects.
- Add the block below to
.claude/settings.json, and commit it to share it with your team. - For just you, use
.claude/settings.local.json. For all your projects, use~/.claude/settings.json. - In a session, run
/hooksand check the hook is listed under PreToolUse. - Ask Claude to push the current branch.
Your config{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "if": "Bash(git push *)", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/dry-run-push.sh", "args": [] } ] } ] } }You should see a
git push --dry-runcall, and git's report of what it would push, with nothing pushed.The hook isn't listed in
/hooks?Check the JSON for a missing comma or bracket. Edits are normally picked up within seconds. If not, restart the session.
The
iffield runs the hook only forgit pushcommands, so other Bash calls pass through untouched.- Add the block below to
Step 3: Load git context at session start
4 minWhatever a SessionStart hook prints goes into Claude's context before your first prompt.
- In the same
hooksblock, add theSessionStartentry below next toPreToolUse. - Quit, then run
claudeagain to start a new session. - Ask Claude which branch you're on.
Add inside "hooks""SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "echo \"Branch: $(git branch --show-current). Uncommitted files: $(git status --porcelain | wc -l | tr -d ' ')\"" } ] } ]You should see your branch name in the answer right away, with no git command run.
Claude doesn't know the branch?
The
startupmatcher fires only on a new session. After/clearor a resume, the matcher isclearorresume, so start a fresh session.To react when a file such as
.envchanges, add aFileChangedhook. Its matcher lists the file names to watch.- In the same
Step 4: Run tests in the background
6 minA background hook lets Claude keep working while slow checks run, and it can wake Claude when they fail.
- Save the script below as
.claude/hooks/test-after-edit.sh, then runchmod +xon it. - Add a
PostToolUseentry with the matcherEdit|Writethat runs it, in the same shape as step 2. - Add
"asyncRewake": trueto that hook, next to"type"and"command". - Ask Claude for a small change, and keep talking while the tests run.
Your script#!/bin/bash # Runs the tests after an edit. Exit code 2 wakes Claude with the failure. if ! OUT=$(npm test 2>&1); then echo "Tests failed after the last edit:" >&2 echo "$OUT" | tail -n 30 >&2 exit 2 fi
You should see no wait after each edit, then a message from Claude with a fix if a test fails.
Claude never hears about failing tests?
The script must write the failure to stderr and exit with code 2. Press Ctrl+O to show background hook messages while you check.
"async": truealso runs a hook in the background, but its results only reach Claude on your next turn.- Save the script below as
Step 5: Give a skill its own model and effort
5 minSkill settings let a quick job run on a smaller, faster model while your session keeps its own.
- Create
.claude/skills/changelog/and save the text below in it asSKILL.md. - Type
/changelogin a session. - For deep work in another skill, set
model: opusandeffort: highinstead.
Your SKILL.md--- name: changelog description: Draft a changelog entry from the commits since the last tag. Use when asked for release notes or a changelog. model: haiku effort: low --- 1. Run `git log $(git describe --tags --abbrev=0)..HEAD --oneline`. 2. Group the commits under Added, Changed and Fixed. 3. Write one plain line per change, for users, not developers.
You should see a changelog draft, and your usual model back when you send your next prompt.
The skill runs on your usual model?
Check that
modelandeffortsit between the two---lines, spelled exactly. Your organization may also block that model.A skill can carry its own
hookstoo. They start when the skill runs, andonce: trueremoves one after its first successful run.onceworks only in skills.- Create
You get
Hooks that turn pushes into dry runs, brief Claude at startup and test in the background, plus a skill on its own model
Try next
Did this guide work for you?
Thanks. It helps us fix the steps.


