Git And Collaboration Workflows
Git Hooks
Git hooks are executable programs triggered at specific points in local Git commands or server-side receive operations.
Why This Matters
Hooks can provide fast feedback and enforce repository policy, but local hooks are installed per clone and many can be bypassed. They complement committed scripts and remote checks rather than replacing them.
Working Model
Local hooks include pre-commit, commit-msg, and pre-push. Receive-side hooks include pre-receive, update, and post-receive. A non-zero exit from a blocking hook stops that Git operation.
Practical Rules
- Keep hook logic thin and call versioned project scripts.
- Choose checks that match the hook's latency budget.
- Print actionable failures and preserve the underlying tool exit code.
- Use
core.hooksPathwhen a repository-managed hook directory is appropriate. - Repeat important policy in CI or server controls.
Failure Modes
- Assuming hooks are copied automatically by
git clone. - Putting the full slow test suite in every pre-commit hook.
- Relying on a bypassable local hook for security or compliance.
- Writing hooks that mutate staged content unexpectedly.
Verification
- Confirm the hook is executable and the effective hooks path is correct.
- Trigger both passing and failing cases.
- Test paths with spaces and partial staging.
- Verify remote policy still rejects invalid changes when hooks are bypassed.
Official References
What You Should Be Able To Do
After this lesson, you should be able to explain the local and server hook lifecycle, exit behavior, installation limits, and appropriate enforcement boundaries, choose a suitable approach for a real PHP project, and verify the result instead of relying on assumptions.
Hooks Are Local Automation At Git Boundaries
Git hooks are scripts that run at specific points in Git's workflow. A pre-commit hook can inspect staged content before a commit is created. A commit-msg hook can check the commit message. A pre-push hook can run checks before objects are sent to a remote. Server-side hooks can enforce policy in a central repository. Each hook runs at a boundary where a developer is trying to change repository state.
Hooks are useful because they provide fast feedback close to the action. A PHP project can run formatting on staged files, reject accidental secrets, lint changed PHP files, prevent conflict markers, or remind developers to update generated files. The goal is not to move all CI into hooks. The goal is to catch cheap, local mistakes before they waste review time.
Know Which Content A Hook Sees
A pre-commit hook should usually inspect staged content, not whatever happens to be in the working tree. A developer can have unstaged experiments that are not part of the commit. Running tools against the working tree may report errors unrelated to the commit, or worse, pass code different from what will be recorded.
Use Git commands that read staged files when the distinction matters. Some hook frameworks handle this for you. If a formatter modifies files, the hook should either restage the intended changes or fail and ask the developer to review and stage them. Automatically staging broad changes can hide unexpected edits.
Pre-push hooks see commits being pushed and can run broader checks, but they should still be reasonably fast. A hook that takes several minutes every push teaches developers to bypass it. Put expensive cross-environment checks in CI where they are reproducible and visible to the team.
Hooks Are Not A Security Boundary
Local hooks can usually be bypassed with flags such as --no-verify, by misconfiguration, or by using another client. They are developer convenience and guardrails, not final enforcement. Anything required for repository integrity, security, or release approval must also run in a remote quality gate controlled by the project.
That does not make local hooks pointless. A secret scanner in pre-commit can stop a mistake before it leaves the machine. Remote secret scanning remains necessary because a local hook might be missing. The two controls work together.
Installing And Sharing Hooks
Git does not clone .git/hooks from the repository. To share hook behavior, teams normally use a hook manager, configure core.hooksPath, or provide an installation command. The chosen approach should be documented in onboarding. Developers should be able to run one command and know which hooks are active.
Hook scripts belong in the repository when they are part of the workflow. Store them under a visible directory such as tools/git-hooks/ or use an established tool. Avoid instructions that ask every developer to paste scripts manually into .git/hooks; that creates drift and makes updates unreliable.
For PHP projects, hooks often call Composer scripts. That keeps the command in one place and lets CI reuse it. For example, a hook can invoke composer lint-staged or a project script that discovers changed PHP files. Make the underlying command runnable without the hook so failures are easy to reproduce.
Designing Good Hook Behavior
A good hook is deterministic, fast, and clear. It prints the command it ran or a useful error. It exits non-zero only for problems the developer can act on. It avoids network dependencies unless the hook is explicitly designed for them. It respects platform differences or documents required tools.
Keep hooks narrow. Pre-commit is good for staged syntax, formatting, conflict markers, secret patterns, and simple metadata. Pre-push can run focused tests or static analysis. CI should run the full matrix, database-backed checks, browser tests, and deployment packaging. If everything runs everywhere, developers lose time and still may not get better evidence.
Handling Generated Files
Hooks can help keep generated files honest. A pre-commit hook might run an OpenAPI generator and fail if output differs, or check that a migration list is sorted. Be careful with expensive generators and platform-specific output. If generation requires Docker, network access, or secrets, CI may be a better enforcement point.
When a hook modifies generated files, the developer should inspect the diff. A hook that silently changes large files and commits them can turn a small edit into an unreviewed artifact update.
Failure And Bypass Policy
Document when bypassing hooks is acceptable. Emergency commits, broken local tooling, or partial work-in-progress may justify --no-verify, but the pull request should say which checks were skipped and CI must catch required failures. If many developers bypass a hook routinely, fix the hook rather than blaming the team.
Hooks should also fail gracefully when dependencies are missing. A message such as "Run composer install to enable PHPStan checks" is more useful than a shell stack trace. If a hook is required by policy, onboarding should install the dependency before the first commit.
Testing Hooks
Test hooks like any other project script. Create fixtures with staged good files, staged bad files, unstaged changes, filenames with spaces, deleted files, and partially staged files. Run the hook in CI if it is important enough to maintain. At minimum, test the underlying command.
For shell scripts, use strict modes carefully and quote paths. Git repositories contain filenames that surprise simple loops. Prefer null-delimited file lists when processing arbitrary paths. A hook that works only for simple filenames will eventually fail in a real project.
After this lesson, you should be able to choose the right Git hook for a workflow boundary, inspect staged content correctly, share hooks through repository-managed tooling, keep required policy in remote gates, design clear failure behavior, and test hooks as maintainable project automation.
Hook Review Checklist
Before adopting a hook, answer who owns it, how it is installed, how it is updated, what command it runs, how long it normally takes, and which remote gate enforces the same required policy. Then test it with staged files, unstaged files, deleted files, filenames containing spaces, and a missing dependency.
For PHP syntax hooks, lint only files that still exist and are staged for commit. For formatting hooks, decide whether the hook changes files or only reports failure. For secret hooks, include allowlist handling for documented test fixtures so developers do not learn to delete the scanner when it complains.
A hook should improve flow. If it blocks commits for unrelated working-tree experiments or prints output that does not identify the file and command, fix the hook. The quality of the developer experience determines whether the team keeps it enabled.
Practice Exercise
Create a pre-commit hook in a temporary repository that rejects staged PHP files containing conflict markers. Test it with a clean staged file, a staged bad file, an unstaged bad file, and a deleted file. The hook should reject only the commit that would actually record the marker.
Then add a pre-push hook that runs a fast unit test command. Make it print the command and explain how to bypass it for an emergency. Finally, confirm that the same test still runs in CI, because local bypass must not remove shared evidence.
A final review should include one bypassed local hook and one remote failure, proving the team still has shared enforcement. Run both checks.
Practice
Practice: Design A Pre-Commit Hook
Design a fast pre-commit hook for a PHP repository with formatting and syntax checks.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Have the hook call committed scripts that inspect staged PHP files, run syntax and formatting checks without rewriting them, print exact failures, and exit non-zero. Keep broader analysis and tests for pre-push or CI.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.
Practice: Validate Commit Messages
Create a policy shape for a commit-msg hook that requires a useful subject without making commits fragile.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Read the message file argument, ignore comment lines, reject an empty or overlong subject and known placeholder text, explain the expected form, and return non-zero. Keep the same policy in remote automation if it is mandatory.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.
Practice: Separate Local And Remote Enforcement
Classify formatting, secret scanning, full tests, branch protection, and deployment approval between local and remote controls.
Your answer must:
- state the intended outcome;
- show the commands, data flow, or implementation shape;
- identify at least one unsafe alternative;
- explain how the result will be verified.
Show solution
Use local hooks for fast formatting, syntax, and optional secret feedback. Run authoritative secret scanning and full tests remotely. Enforce protected branches and deployment approval through the hosting platform or controlled server.
The important part is not memorising one command or vendor screen. The solution makes the invariant, failure behavior, and verification evidence explicit.