Post-Merge Hooks: Automating Tasks After Every Merge
AI generated
git
HEAD
Git
Post-Merge Hooks
Automating recurring tasks after every merge

Forgetting to update Composer after every pull costs time chasing cryptic error messages. The post-merge hook handles routine tasks like that automatically, without anyone having to remember.

10 min read Git Hooks Automation

1. What the post-merge hook is

The post-merge hook is a client side script that Git runs automatically once a merge finishes successfully. That includes both explicit git merge calls and git pull, since pull internally consists of fetch plus merge.

Unlike server side hooks, post-merge only ever runs locally, on the machine of the developer who performed the merge. There is no way to enforce it centrally for everyone involved from the server.

The script lives at .git/hooks/post-merge by default and has to be executable for Git to invoke it at all. Without execute permission, the hook is silently skipped, with no error message.


# Create the hook file and make it executable
touch .git/hooks/post-merge
chmod +x .git/hooks/post-merge

# Minimal test hook
echo '#!/bin/sh
echo "Merge finished, running follow up tasks"' > .git/hooks/post-merge

2. Typical use cases

The most common use for post-merge is automatically updating dependencies. If a merge changes composer.lock or package-lock.json, a matching install command runs right away in the background, without the developer having to remember it manually.

A second common use case is clearing local caches. In Magento projects, a merge often touches layout XML, configuration, or class structure, so an automatic hint or a direct cache clean call avoids many small follow up bugs.

Triggering database migrations, or at least a clear notice that new migrations exist, can also be automated through the hook, even though actually running them should usually still require manual confirmation for safety reasons.


#!/bin/sh
# Only run when the Composer lock file actually changed
if git diff --name-only ORIG_HEAD HEAD | grep -q "composer.lock"; then
    echo "composer.lock changed, running composer install"
    composer install
fi

3. Basic structure and the script's parameter

The post-merge hook receives exactly one parameter: a 1 if the merge ran in squash mode, otherwise a 0. That lets you define different behavior for regular merges and squash merges, if that distinction matters to the team.

Git does not evaluate the script's exit code to undo the merge itself, the merge has already fully completed by that point. A failing hook can no longer prevent the preceding merge, it can only emit a warning.

Because the hook runs after every merge, even very small ones, it should decide quickly whether any action is needed at all before triggering expensive commands like a full dependency install.


#!/bin/sh
SQUASH_FLAG="$1"

if [ "$SQUASH_FLAG" = "1" ]; then
    echo "Squash merge detected, skipping automated actions"
    exit 0
fi

echo "Regular merge, checking for relevant changes"

4. Distinguishing fast forward merges from real ones

Not every merge creates its own merge commit. In a fast forward merge, the pointer of the current branch simply moves ahead, without a new commit being created. The post-merge hook runs in both cases, regardless of whether an actual merge commit was created.

To detect changes introduced by the merge, comparing the environment variable ORIG_HEAD, which Git sets before the merge, against the current HEAD works well. That approach works equally for fast forward and real merges.

A common mistake is assuming there is always a second parent to compare against. In a fast forward merge, no second merge parent exists, which is why comparing against HEAD^2 fails in that case, and ORIG_HEAD should be used instead.


#!/bin/sh
# Robust comparison, works for fast forward and real merges alike
CHANGED_FILES=$(git diff --name-only ORIG_HEAD HEAD)

if echo "$CHANGED_FILES" | grep -q "^composer.lock$"; then
    composer install
fi

5. Distributing hooks across a team

Files inside .git/hooks/ are never versioned by Git and therefore never shared automatically with the repository. Every developer would have to manually copy the hook into their own local copy, something that gets forgotten quickly in practice.

The cleanest solution is the core.hooksPath configuration option, which lets you point Git to an alternative, versioned directory for hooks. If that directory lives inside the repository itself, for example under .githooks/, the hook ships automatically with every clone and every pull.

Alternative tools like Husky solve the same problem for Node based projects by setting core.hooksPath automatically while installing dependencies. For pure PHP or Magento projects, manual configuration through a setup script is usually entirely sufficient.


# Create a versioned hook directory and activate it
mkdir -p .githooks
git mv .git/hooks/post-merge .githooks/post-merge
git config core.hooksPath .githooks

# Document it in the README or setup script so everyone runs it
echo "git config core.hooksPath .githooks" >> setup.sh

6. Keeping security and performance in mind

Because post-merge scripts run with the full privileges of whoever triggers them, the content of a shared hook directory should be treated as seriously in code review as any other change to the repository. A tampered hook could execute arbitrary commands.

Long running actions inside the hook, for example a full reinstall of every dependency on every merge regardless of what actually changed, frustrate a team quickly. A hook should only become active when the relevant files actually changed.

A hook should never fail in a blocking way when an action fails for a non critical reason, for example missing internet access. A clear warning message is usually better than an aborted merge that slows the developer down for no good reason.


#!/bin/sh
if ! composer install 2>/tmp/composer-error.log; then
    echo "Warning: composer install failed, please check manually"
    cat /tmp/composer-error.log
fi

7. Interaction with post-checkout and post-rewrite

The post-merge hook does not exist in isolation. post-checkout runs after a branch switch and is the right place for actions relevant when switching between branches with different dependencies, independent of any merge.

post-rewrite fires on operations that rewrite commits, such as git rebase or git commit --amend. Anyone who wants the same follow up work after both a merge and a rebase should move the actual logic into a shared script called from both hooks.

That separation avoids duplicated code and ensures the logic only ever needs updating in one place, regardless of which Git operation ultimately triggered the change.


#!/bin/sh
# post-merge and post-rewrite both call the same shared script
DIR="$(git rev-parse --show-toplevel)"
sh "$DIR/.githooks/lib/sync-dependencies.sh"

8. A practical example for a Magento project

In a Magento project, a merge frequently changes composer.lock, module configuration files, or db_schema.xml definitions. A hook can emit a clear, specific recommendation for each of these changes instead of silently running everything automatically.

Fully automatic setup:upgrade execution directly inside the hook is risky in most projects, since the command can run for a while and can conflict with other people using the database at the same time. A clear notice in the terminal is the safer approach here.

Combined with core.hooksPath, a hook like this ensures no team member forgets necessary follow up work after a merge, without executing critical commands in an uncontrolled way.


#!/bin/sh
CHANGED=$(git diff --name-only ORIG_HEAD HEAD)

echo "$CHANGED" | grep -q "composer.lock" && echo "Note: run composer install"
echo "$CHANGED" | grep -q "db_schema.xml" && echo "Note: run bin/magento setup:upgrade"

9. Debugging and disabling hooks

Unlike pre-commit or pre-push, there is no way to bypass post-merge using --no-verify, since that option is only meant for hooks that can prevent an operation. A merge has already completed by the time this hook runs.

For targeted testing, the hook can be invoked directly without performing an actual merge. That makes it quick to find logic errors without actually changing the repository state.

For permanent deactivation, it is enough to make the file non executable or point core.hooksPath to an empty directory. Detailed tracing of hook execution can additionally be enabled through standard shell debugging options right inside the script itself.


# Invoke the hook manually, without an actual merge
sh .git/hooks/post-merge 0

# Enable shell built in tracing inside the hook
#!/bin/sh
set -x
Hook Timing Blocking Typical action
post-merge After a completed merge or pull No Update dependencies, clear cache
post-checkout After a branch switch No Adjust the environment to the new branch
post-rewrite After rebase or amend No Same follow up work as post-merge on rewritten commits
pre-push Before pushing to the remote Yes Enforce tests and linting before the push

Mironsoft

Git workflows, branching strategies, and CI hooks

Chaotic Git history and unclear branching rules across the team?

We set up clean Git workflows, clarify branching strategies for the team, and automate quality checks via Git hooks and CI pipelines so the history stays traceable.

Workflow Audit

Review the existing branching strategy and merge practice for weak spots.

Hook Automation

Set up pre-commit and pre-push hooks for linting, tests, and commit conventions.

Team Training

Teach rebase, cherry-pick, and conflict resolution hands-on across the team.

10. Summary

Post-Merge Hooks

Location

.git/hooks/post-merge, must be executable

Parameter

1 on a squash merge, otherwise 0

Distribution

core.hooksPath for a versioned hook directory

Comparison

ORIG_HEAD against HEAD covers fast forward and real merges

11. FAQ: Post-Merge Hooks

1Exactly when does the post-merge hook run?
Immediately after a merge completes successfully, for both an explicit git merge and a git pull, since pull internally consists of fetch and merge.
2Can the post-merge hook undo a merge?
No, the merge has already fully completed by the time the hook runs. The hook can only react after the fact and emit warnings, it can no longer block the change.
3Why is my post-merge hook not running?
The most common cause is a missing execute permission on the script file. Without chmod +x on the file, Git silently skips the hook without showing an error.
4How can I share post-merge hooks across the whole team?
The core.hooksPath configuration option lets you point Git to a versioned directory inside the repository as the hook source. That way the hook is distributed to every team member automatically with every clone and every pull.
5What does the parameter Git passes to the hook mean?
The only parameter passed is a 1 if the merge ran in squash mode, otherwise a 0. That lets the script define different behavior for regular and squash merges.
6How do I detect which files changed as a result of the merge inside the hook?
Comparing the ORIG_HEAD variable, which Git sets before the merge, against the current HEAD returns the list of changed files, regardless of whether it was a fast forward or a real merge.
7Is a post-merge hook also active on a fast forward merge?
Yes, the hook runs in both cases. Since no second merge parent exists on a fast forward merge, ORIG_HEAD should be used for the file comparison instead of a parent commit.
8How does post-merge differ from post-checkout?
post-merge runs after a completed merge or pull, while post-checkout runs after every branch switch, regardless of whether a merge happened. Both can be combined for shared follow up work.
9Can I bypass the post-merge hook with --no-verify?
No, --no-verify only applies to hooks that can prevent an operation, such as pre-commit or pre-push. The post-merge hook runs after an operation that has already completed and does not recognize that option.
10What risks come with automated actions inside a post-merge hook?
A tampered, shared hook runs with the full privileges of whoever triggers it and should therefore be reviewed just as seriously as any other change to the repository. Long running actions without a condition check can also slow a team down unnecessarily.