Home / Alt manpages / git-hook(1)

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

Run and Test Git Hooks Safely with git hook

You will finish with a small, repeatable way to run a Git hook by name, pass it arguments or a file on standard input, and explain why Git did or did not find it. 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. 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 the arguments for you. The hook receives exactly the values after the separator. The arguments depend on the lifecycle hook: for example, 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 that 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.

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

  • You confirmed the installed Git version and git hook run syntax.
  • You know whether the hook comes from .git/hooks or core.hooksPath.
  • You passed hook arguments after -- and streamed stdin only when the hook expects it.
  • You checked executable permissions and distinguished a missing hook from a failing hook.
  • You tested with an ordinary user in a disposable or reviewed repository.
  • You removed any temporary core.hooksPath setting and kept the test hook only if it is intentional.