Home / Alt manpages / git-hook(1)

  • git-hook(1)
  • User command
  • linux

Run and Test Git Hooks Safely with git hook

git hook run lets you invoke a Git hook by name from a script, instead of waiting for Git to trigger it. It also tells you plainly when a hook cannot be found. The examples match Git 2.43.0 from the Ubuntu git-man package installed here.

Allow about fifteen minutes. You need Git, a repository you can safely test, and permission to read its hooks directory. This guide runs hooks, so treat a hook as executable code: read it before testing it, and do not run an unfamiliar hook in a repository containing secrets or valuable uncommitted work. The examples do not need sudo.

1. Check the installed interface

Start with the local command and package version. This is read-only:

$ git --version
git version 2.43.0
$ git hook -h
usage: git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]

The only subcommand in this installed interface is run. It is a command interface for scripts that need to invoke a hook. It does not create a hook, enable a hook, or decide which lifecycle point is appropriate for your project.

Checkpoint

Confirm the version before copying examples into automation. Option details can differ in another Git release.

2. Put a harmless test hook in the right directory

Git looks for hooks in $GIT_DIR/hooks by default. In a normal working repository, that means the .git/hooks directory. A hook is ignored unless its executable bit is set.

In a disposable repository, create a hook that reports its working directory, arguments and standard input:

$ git init /tmp/hook-demo
$ cd /tmp/hook-demo
$ cat > .git/hooks/demo <<'EOF'
#!/bin/sh
printf 'hook cwd: %s\n' "$PWD"
printf 'arg 1: %s\n' "$1"
printf 'arg 2: %s\n' "$2"
printf 'stdin: '
cat
EOF
$ chmod +x .git/hooks/demo

The hook name is the filename, demo. Git does not require a filename extension. The shebang selects the interpreter, and chmod +x makes the file runnable. Warning: read the file before running it. A real hook can edit files, reject commits, send data elsewhere, or run privileged commands.

Run it without input first:

$ git hook run demo
hook cwd: /tmp/hook-demo
arg 1:
arg 2:
stdin:
$ printf 'exit status: %s\n' "$?"
exit status: 0

Git changes the hook's working directory to the working-tree root for a non-bare repository. A hook in a bare repository runs from $GIT_DIR. Push-side hooks such as pre-receive, update and post-receive run in $GIT_DIR even when the repository has a working tree.

3. Pass hook arguments after the separator

Put positional arguments after a mandatory --. This separates arguments intended for the hook from options understood by git hook run:

$ git hook run demo -- first-value 'second value'
hook cwd: /tmp/hook-demo
arg 1: first-value
arg 2: second value
stdin:
  • Without the separator, a value beginning with a dash can be interpreted as a Git option.
  • Quote values that contain spaces or shell punctuation. Do not build the whole command as one unquoted string when an argument came from a user or another process.
  • Git does not add arguments for you. The hook receives exactly the values after the separator, and those depend on the lifecycle hook: commit-msg receives the commit message file, while pre-commit receives no parameters. See the installed githooks(5) entry for the contract of the hook you are invoking.

4. Stream a file into standard input

Use --to-stdin=<path> when the hook should receive a complete file on standard input. The file is streamed from its beginning to EOF:

$ printf 'input supplied by the caller\n' > hook-input.txt
$ git hook run --to-stdin=hook-input.txt demo -- checked
hook cwd: /tmp/hook-demo
arg 1: checked
arg 2:
stdin: input supplied by the caller

The path in this example is relative to the directory where you run Git, not a path invented inside the hook. Use an absolute path when a script's current directory is uncertain. Check the input before running a hook that can make changes:

$ test -r hook-input.txt && wc -c < hook-input.txt
29

Do not assume that an ordinary lifecycle hook expects stdin merely because this option exists. Follow that hook's documented interface: supplying stdin is useful for a hook with an input protocol or for a controlled test, but it does not turn arbitrary text into valid input.

5. Select a different hooks directory

core.hooksPath replaces the default $GIT_DIR/hooks location. Inspect the effective setting before troubleshooting a hook that appears to be missing:

$ git config --show-origin --get core.hooksPath
$ git rev-parse --git-dir
.git

No output from the first command means the default directory is in use. A relative configured path is interpreted relative to the directory where Git normally resolves repository configuration. For a local test, set a repository-local path and create the executable there:

$ mkdir -p .githooks
$ printf '%s\n' '#!/bin/sh' 'echo custom-hook-ok' > .githooks/demo
$ chmod +x .githooks/demo
$ git config core.hooksPath .githooks
$ git hook run demo
custom-hook-ok

This changes the repository configuration. Undo it when the test is complete:

$ git config --unset core.hooksPath
$ git config --show-origin --get core.hooksPath
$ git hook run demo
hook cwd: /tmp/hook-demo

The final run works because Git has returned to .git/hooks/demo. If the setting was inherited from a global or system configuration, --unset may report that there is no local value. In that case, inspect all levels with git config --show-origin --show-scope --get-all core.hooksPath and remove only the setting you intentionally added. Do not delete a shared hooks directory to fix a lookup problem.

6. Handle missing and ignored hooks

A missing hook is normally an error:

$ git hook run not-installed
error: cannot find a hook named not-installed
$ printf 'exit status: %s\n' "$?"
exit status: 1

Use --ignore-missing only when the caller deliberately wants an optional hook. It returns zero without producing an error:

$ git hook run --ignore-missing not-installed
$ printf 'exit status: %s\n' "$?"
exit status: 0

That option does not ignore a hook that exists but fails. A non-executable hook is ignored by Git, so check both the file and its mode:

$ hook_path=.git/hooks/demo
$ test -f "$hook_path" && ls -l "$hook_path"
-rwxr-xr-x 1 you you 123 Sep 24 10:00 .git/hooks/demo
$ test -x "$hook_path" && echo executable
executable

Use the actual size, owner and timestamp from your machine. If a hook is present but not executable, fix the mode only after reviewing its contents: chmod u+x .git/hooks/name. A hook's non-zero exit status is passed back to Git, so a failing hook can be the expected result of a policy check rather than a failure of git hook run itself.

7. Know what the hook can see

Git exports repository-related variables such as GIT_DIR and GIT_WORK_TREE so commands inside a hook can find the current repository. If the hook invokes Git on another repository, clear Git's local environment first, for example:

$ unset $(git rev-parse --local-env-vars)
$ git -C /path/to/other-repository status --short

That command changes only the current shell's environment. Prefer a subshell in a script if later commands need the original repository context. Replace /path/to/other-repository with a repository you trust and can access; do not use a path supplied by an untrusted caller without validating it.

Security warning

Hooks are ordinary programs. They can observe their environment, read files available to the invoking user, and change repository state. Run a hook as the account that normally runs the Git operation. Do not use sudo git hook run ... to make a permission error disappear: that changes ownership and environment assumptions and can make the test misleading.

Done means

  • Confirmed the installed Git version and git hook run syntax.
  • Know whether the hook comes from .git/hooks or core.hooksPath.
  • Passed hook arguments after -- and streamed stdin only when the hook expects it.
  • Checked executable permissions and told apart a missing hook from a failing hook.
  • Tested with an ordinary user in a disposable or reviewed repository.
  • Removed any temporary core.hooksPath setting and kept the test hook only if it is intentional.