Skip to content

LearnTutorials

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.

By Recaply EditorsAdvanced24 min5 stepsUpdated

A hand points at the fork of a toy railway track as a small train rolls down one branch

What you need

On Windows, hooks run in Git Bash. Without Git Bash they run in PowerShell, and these scripts need rewriting.

  1. Step 1: Write a script that rewrites a command

    5 min

    A PreToolUse hook sees each tool call before it runs, and it can change the call's input.

    1. In your project, run mkdir -p .claude/hooks.
    2. Save the script below as .claude/hooks/dry-run-push.sh.
    3. Run chmod +x .claude/hooks/dry-run-push.sh so it can run.
    4. 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-run as the new command.

    It says jq: command not found?

    Install jq with your system's package manager, then run the test again.

    updatedInput replaces the whole input, so the script keeps the other fields and changes only command.

  2. Step 2: Register the hook

    4 min

    The settings file you pick decides who gets the hook: your team, just you, or all your projects.

    1. Add the block below to .claude/settings.json, and commit it to share it with your team.
    2. For just you, use .claude/settings.local.json. For all your projects, use ~/.claude/settings.json.
    3. In a session, run /hooks and check the hook is listed under PreToolUse.
    4. 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-run call, 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 if field runs the hook only for git push commands, so other Bash calls pass through untouched.

  3. Step 3: Load git context at session start

    4 min

    Whatever a SessionStart hook prints goes into Claude's context before your first prompt.

    1. In the same hooks block, add the SessionStart entry below next to PreToolUse.
    2. Quit, then run claude again to start a new session.
    3. 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 startup matcher fires only on a new session. After /clear or a resume, the matcher is clear or resume, so start a fresh session.

    To react when a file such as .env changes, add a FileChanged hook. Its matcher lists the file names to watch.

  4. Step 4: Run tests in the background

    6 min

    A background hook lets Claude keep working while slow checks run, and it can wake Claude when they fail.

    1. Save the script below as .claude/hooks/test-after-edit.sh, then run chmod +x on it.
    2. Add a PostToolUse entry with the matcher Edit|Write that runs it, in the same shape as step 2.
    3. Add "asyncRewake": true to that hook, next to "type" and "command".
    4. 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": true also runs a hook in the background, but its results only reach Claude on your next turn.

  5. Step 5: Give a skill its own model and effort

    5 min

    Skill settings let a quick job run on a smaller, faster model while your session keeps its own.

    1. Create .claude/skills/changelog/ and save the text below in it as SKILL.md.
    2. Type /changelog in a session.
    3. For deep work in another skill, set model: opus and effort: high instead.
    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 model and effort sit between the two --- lines, spelled exactly. Your organization may also block that model.

    A skill can carry its own hooks too. They start when the skill runs, and once: true removes one after its first successful run. once works only in skills.

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

Did this guide work for you?

Sources