A git hook is a script that Git runs automatically at a point in its workflow, such as before a commit or after a checkout, and that makes hooks both a cheap security control and a way to run code on a developer's machine. Teams use pre-commit hooks to stop secrets, keys and unformatted code reaching a repository; attackers have used hook execution during clone to run code before a developer could inspect anything. AI coding agents add a third layer: Claude Code and Cursor now have their own hook systems that run shell commands around the agent's actions, configured by files that can be committed to the repository. This guide explains what git hooks are and where they run, what they can and cannot enforce, how to deploy pre-commit secret scanning properly, the risks of hooks as an execution path, and how agent hooks fit. It is written for developers, platform and DevSecOps teams. Scanner choice and remediation of leaked secrets are covered in the Git Secret Scanning Guide.
Key takeaways
- Client-side hooks are advisory. They live on the developer's machine, are not enforced by the remote, and Git documents that pre-commit, commit-msg and pre-merge-commit can be bypassed with --no-verify. Enforce the same checks again server-side or in CI.
- Hooks are code execution. A hook runs with the user's permissions and environment, including cloud credentials, tokens and SSH agent access. Treat hook sources and hook configuration as untrusted until reviewed.
- Pin what you run. With the pre-commit framework, each hook comes from a repository at a rev; pin it to a commit hash (pre-commit autoupdate --freeze) rather than a movable tag.
- Keep Git patched. CVE-2024-32002 let a crafted repository with submodules write a hook that ran during a recursive clone on case-insensitive filesystems that support symlinks, with no chance to inspect it.
- Agent hooks need the same governance. Claude Code and Cursor hooks run commands around agent actions and can be defined in project files; administrators can restrict them to managed configuration.
What git hooks are and where they run
The Git documentation states that hooks live by default in $GIT_DIR/hooks (usually .git/hooks), that the location can be changed with the core.hooksPath configuration variable, and that hooks without the executable bit are ignored. A fresh .git/hooks directory contains only .sample files, which do nothing until renamed. The .git directory is not part of what you commit, so hooks are not shared through a normal push; teams share them through a tracked directory plus core.hooksPath, or through a framework that installs them.
| Hook | Runs on | Invoked by | Security use | Bypass |
|---|---|---|---|---|
| pre-commit | Developer machine | git commit | Secret scanning, linting, blocking large or binary files | --no-verify (documented) |
| commit-msg | Developer machine | git commit, git merge | Enforce ticket references or sign-off text | --no-verify (documented) |
| pre-merge-commit | Developer machine | git merge | Same checks as pre-commit for merges | --no-verify (documented) |
| pre-push | Developer machine | git push | Scan the commits about to be pushed; block pushes to protected branches | Local control; the developer can remove or skip it |
| post-checkout, post-merge | Developer machine | git checkout or switch, git clone (unless --no-checkout), git merge | Environment set-up; also an execution path an attacker wants | Not a control |
| pre-receive, update, proc-receive | Git server | git-receive-pack when it handles a push | Reject pushes that contain secrets or break policy | Cannot be skipped by the pusher |
| post-receive, post-update | Git server | git-receive-pack after a push | Notifications, audit, triggering scans | Not a gate |
On hosted platforms such as GitHub and GitLab you usually cannot install raw server-side hooks; the equivalents are push protection, push rules, protected branches and required status checks. Self-managed Git servers can run pre-receive hooks directly.
What hooks can and cannot enforce
- Can: give fast feedback before a secret ever enters history, which is far cheaper than rewriting history and rotating a credential after a push. Most leaks are accidents, and a pre-commit hook catches the accident at the keyboard.
- Cannot: guarantee anything. The developer controls their own machine. Git documents --no-verify for pre-commit, the pre-commit framework documents a SKIP environment variable listing hook IDs to skip, and gitleaks documents SKIP=gitleaks to disable its hook. A hook a developer never installed does nothing.
- Conclusion: treat client-side hooks as a usability layer and put the enforcing control where the developer cannot turn it off: server-side hooks, platform push protection and CI checks on every pull request.
Deploying pre-commit secret scanning
- Choose a framework. The pre-commit framework reads a .pre-commit-config.yaml with a list of repos, each with a repo URL, a rev (the revision or tag to clone at) and a list of hooks by id. Repository-local hooks use the sentinel repo: local.
- Add a secret scanner. gitleaks documents this configuration:
Check the project for the current release before you pin it.repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.24.2 hooks: - id: gitleaks - Pin to a hash. Run
pre-commit autoupdate --freeze, which stores frozen hashes in rev instead of tag names. A tag can be moved to malicious code; a commit hash cannot. - Install for the right hook types.
pre-commit installinstalls the pre-commit hook; add pre-push withpre-commit install --hook-type pre-commit --hook-type pre-push, so a second scan runs on everything about to leave the machine. - Baseline the repository. Run
pre-commit run --all-filesonce to find existing findings, triage them, and fix or document them before enforcing. - Run the same config in CI. The pre-commit documentation describes run --all-files as useful for CI. Make it a required check on pull requests, so a skipped local hook is caught before merge.
- Turn on platform enforcement. Enable push protection or a pre-receive secret scan so a push containing a known secret pattern is rejected outright.
- Log bypasses. Where your platform supports push protection bypass reasons, require one and review them. See the Git Secret Scanning Guide.
Rollback: if a new hook blocks legitimate work, tune the rule or allow-list the false positive in the scanner's configuration and ship the change through review, rather than telling developers to use --no-verify, which teaches the habit you are trying to remove.
Choosing how to manage hooks
| Approach | How it works | Choose when | Security trade-off |
|---|---|---|---|
| Native hooks in .git/hooks | Scripts placed by hand or by a set-up script | One-off personal checks | Not shared or versioned; easy to forget and hard to audit |
| Tracked directory plus core.hooksPath | Hooks committed to the repository; each clone points core.hooksPath at them | Small teams wanting plain shell scripts with no extra tooling | Hook changes arrive through normal code review, but every developer must opt in |
| pre-commit framework | A YAML config names hook repositories, revs and hook IDs; the tool installs and caches them | Polyglot repositories and organisations standardising checks | Pulls third-party code, so pin to hashes and mirror hook repositories |
| Language-specific managers (for example Husky in npm projects) | Installed through the project's package manager | Single-ecosystem projects already using that tooling | Inherits that ecosystem's dependency risk; review install scripts |
| Server-side and platform controls | Pre-receive hooks, push protection, required CI checks | Always, as the enforcing layer | Cannot be skipped by the developer; slower feedback |
Small teams: a tracked hooks directory or a short pre-commit config with a secret scanner, plus the same scan in CI, covers most of the value. Large organisations: publish an approved, hash-pinned pre-commit baseline, mirror the hook repositories internally, distribute agent guard hooks through managed settings and measure adoption per repository.
Hooks as an attack path
| Risk | How it happens | Control |
|---|---|---|
| Hook written during clone | CVE-2024-32002: a crafted repository with submodules could make Git write into .git instead of the submodule worktree and plant a hook that ran during clone, on case-insensitive filesystems with symlink support | Upgrade Git (fixed in 2.45.1, 2.44.1, 2.43.4, 2.42.2, 2.41.1, 2.40.2 and 2.39.4); disabling symlink support blocks the attack; avoid recursive clones of untrusted repositories |
| Poisoned hook repository | A pre-commit config pins a hook repo by tag, and the tag is moved or the repo is compromised | Pin rev to a commit hash; review hook repos like dependencies; mirror them internally |
| Malicious tracked hooks directory | A repository ships a hooks folder and a set-up script that sets core.hooksPath to it | Review set-up scripts; do not run project bootstrap commands from untrusted repos |
| Global hooks firing everywhere | A global core.hooksPath or global pre-commit set-up runs hooks in every repository, including untrusted ones | The pre-commit documentation says a global set-up is not recommended because it can run hooks when interacting with an untrusted repository |
| Credential exposure | Hooks inherit the environment, so they can read cloud credentials, tokens and SSH agent sockets (ATT&CK T1552.001) | Short-lived credentials, no long-lived tokens in environment variables or dotfiles |
| Persistence | An attacker with access to a workstation adds a hook that runs on every commit (event-triggered execution, T1546) | EDR monitoring of changes to .git/hooks and core.hooksPath; file integrity checks on shared hook directories |
Git's own design limits some of this. The git-config documentation describes protected configuration (system, global and command scopes) and states that certain options are only respected there, because a repository's own config cannot be trusted in the same way. Hooks themselves are never transferred by clone or fetch, which is why the CVE-2024-32002 class of bug, writing into .git, mattered so much.
Hooks in AI coding agents
AI coding agents have adopted the hook idea for their own loops, and the configuration often lives in the repository, next to the code an agent or developer is opening.
| Aspect | Claude Code | Cursor |
|---|---|---|
| What a hook is | A user-defined shell command, HTTP endpoint, MCP tool call, LLM prompt or subagent run at lifecycle points | A custom script run at points in the agent loop, exchanging JSON over stdio |
| Example events | SessionStart, UserPromptSubmit, PreToolUse (can block), PostToolUse, Stop, SessionEnd | beforeShellExecution, beforeMCPExecution, beforeReadFile, afterFileEdit, stop |
| Where configured | ~/.claude/settings.json; .claude/settings.json (can be committed); .claude/settings.local.json; managed policy settings; plugins | Enterprise MDM file; team dashboard; project .cursor/hooks.json (runs in any trusted workspace); ~/.cursor/hooks.json |
| Blocking | Exit code 2 blocks on events that can block; other non-zero codes are non-blocking | Exit code 2 or a deny decision blocks; deny wins over ask and allow |
| Failure behaviour | A timed-out PreToolUse hook does not block the tool call | Crashes, timeouts and other non-zero exits fail open unless failClosed: true is set |
| Admin control | allowManagedHooksOnly blocks user, project, local and plugin hooks; strictPluginOnlyCustomization locks hooks to plugins and managed settings | Enterprise and team hook files take priority over project and user files |
- As guardrails: agent hooks are useful security controls: a PreToolUse or beforeShellExecution hook can block destructive commands, and beforeReadFile can stop an agent reading .env files or key material. Claude Code's documentation advises using its permission system rather than a hook for a hard allow or deny, and both products fail open in some failure modes, so design guard hooks to fail closed where the product allows it.
- As an execution path: a project-level hook file is code that runs when you work in that repository. The Amazon Q MCP configuration vulnerability showed the pattern: a repository's configuration file started processes as soon as the folder was opened, and those processes inherited the developer's AWS credentials and tokens. Review agent configuration in pull requests like code, and use workspace trust. See the Coding Agent Configuration File Attacks guide.
- Centralise in organisations: deploy required guard hooks through managed settings, restrict hooks to managed sources where you can, and inventory which repositories carry agent hook files.
Where identity fits
Hooks matter for non-human identity because they run where developer and machine credentials live. A pre-commit secret scanner is the first line of defence against API keys, cloud keys and tokens entering source control; a malicious hook is a credential stealer with the developer's full environment. Prefer short-lived, federated credentials on developer machines, keep secrets out of shell profiles and environment variables where possible, and give AI agents their own scoped credentials rather than the developer's. See the Developer Workstation and IDE Security Guide.
Detection and measurement
- Endpoint: alert on new or modified files under .git/hooks in repositories, changes to core.hooksPath in global config, and hook processes spawning network tools or reading credential files.
- Repository: flag pull requests that add or change .pre-commit-config.yaml, tracked hooks directories, .claude/settings.json or .cursor/hooks.json, and require code owner review for them.
- Platform: track push protection blocks and bypasses, and CI secret scan failures that local hooks should have caught.
Metrics: share of active repositories with a pre-commit config that includes a secret scanner; share of those pinned to commit hashes; secrets caught in CI that a local hook should have caught (a proxy for skipped or uninstalled hooks); push protection bypasses per month; developer machines on a Git version below the patched releases; repositories carrying agent hook files without code owner review.
Common failure patterns
- Hooks as the only control: relying on pre-commit for secret prevention with no server-side or CI check, so --no-verify or a missing install defeats it.
- Tags instead of hashes: pinning hook repositories to tags, so a moved tag silently changes the code that runs on every developer machine.
- Normalising bypass: documentation that tells developers to use --no-verify or SKIP when a hook is noisy.
- Unpatched Git: developer machines and build agents on old Git versions while the team clones external repositories with submodules.
- Global hooks on everything: running hooks in every repository, including untrusted clones, against the pre-commit project's own advice.
- Agent hooks committed without review: project-level agent configuration merged without anyone noticing that it runs commands, the same trust gap the Amazon Q case exposed.
Lessons from incidents
The Amazon Q case involved an MCP configuration file rather than a git hook, but the lesson is the same: files in a repository that a tool executes automatically are code, and they run with everything the developer can reach. CVE-2024-32002 showed the git-native version, where a crafted repository planted a hook during clone. Treat hook files and agent configuration as part of the attack surface you review, patch Git promptly, and make sure a malicious hook would find only short-lived, narrowly scoped credentials.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev. 5 | SA-11, SA-15, CM-3, IA-5(7) | Developer testing, development tools, change control, no embedded static authenticators |
| NIST CSF 2.0 | PR.PS-06 | Secure software development practices integrated and monitored |
| MITRE ATT&CK | T1195.001, T1546, T1552.001, T1204.002 | Compromised development tools, event-triggered execution, credentials in files, malicious files run by users |
Practitioner checklist
- Add a pre-commit config with a secret scanner to every active repository.
- Pin hook repositories to commit hashes with autoupdate --freeze.
- Install pre-commit and pre-push hook types.
- Run the same checks in CI as a required status check.
- Enable push protection or a pre-receive secret scan on the server.
- Patch Git on developer machines and build agents.
- Avoid global hooks and recursive clones of untrusted repositories.
- Require code owner review for hook and agent configuration files.
- Deploy agent guard hooks through managed settings and restrict unmanaged hooks.
- Track bypasses, CI catches and hash-pinning coverage.
Standards and references
- Git: githooks documentation
- Git: git-config documentation
- Git: Security advisory for CVE-2024-32002
- pre-commit: A framework for managing and maintaining multi-language pre-commit hooks
- Gitleaks: Project documentation
- Anthropic: Claude Code hooks reference
- Cursor: Hooks documentation
- MITRE ATT&CK: Compromise Software Dependencies and Development Tools
Related NHI Mgmt Group resources: Git Secret Scanning Guide · Git Repository Security Guide · Coding Agent Configuration File Attacks · AI Coding Agents Security Guide