Why Git Hooks Are a Server-Side Attack Surface
Git hooks are executable scripts that Git runs automatically. On a hosting server, that makes the hooks directory a code-execution primitive for anyone who can write to it.
What a Git HookGit Hook🛡️A script that Git executes automatically when a repository event occurs, such as a push, commit, or index write. Hooks are plain executable files in the repository's hooks directory and run with the privileges of the user running Git, which makes them a code-execution primitive on hosting servers. Actually Is
A Git hook is a program that Git executes on its own when a specific event happens in a repository: before a commit is finalized, after a push is received, when the index is rewritten, and so on. Hooks live as plain files inside the repository's Git directory, under a hooks subdirectory, and Git runs any file there whose name matches an event and which has the executable bit set. There is no signing, no allowlist, and no sandbox. If a file called post-receive exists, is executable, and a push arrives, Git runs it with the privileges of whatever user is running Git.
On a developer's laptop this is convenient: hooks lint code, run tests, and enforce commit message formats. On a server that hosts repositories for other people, the same mechanism is a loaded weapon. The server runs Git constantly, as a single service accountService Account🛡️A non-human operating system or application account under which a service runs. Its permissions define the blast radius of any exploitExploit🛡️Code or technique that takes advantage of a vulnerability to cause unintended behavior, such as gaining unauthorized access. against that service, since attacker code executes with the service account's access to files, secrets, and the network., against repositories whose contents are controlled by users. Any path that lets a user place an executable file into a hooks directory is, by definition, remote code execution.
That is exactly what the Gitea diffpatch vulnerabilityVulnerability🛡️A weakness in software, hardware, or processes that can be exploited by attackers to gain unauthorized access or cause harm. CVE-2026-60004 turned out to be, and why CISA added it to the KEV catalog in August 2026 after cryptojackingCryptojacking🛡️Unauthorized use of a compromised system's CPU or GPU to mine cryptocurrency for the attacker. Typically delivered by an automated dropper after an exploit, it is usually detected through sustained high CPU load rather than through data loss. campaigns started using it.
Working Trees, Bare Repositories, and Where Hooks Live
The distinction that matters is between a normal repository and a bare one. A normal clone has a working tree (the files you edit) plus a hidden .git directory holding objects, refs, config, and hooks. A bare repositoryBare Repository🛡️A Git repository with no working tree; the repository root is itself the Git directory containing objects, refs, config, and hooks. Hosting servers use bare repositories, so a file written to a path like hooks/ inside one lands in Git's control plane rather than in project files. has no working tree; the repository root itself is the Git directory. Hosting servers use bare repositories almost universally because they never need checked-out files, only objects and refs.
The consequence is subtle. In a bare repository, a path like hooks/post-receive is not a project file; it is Git's own hook slot. In a normal repository, a file at hooks/post-receive is just a file in the working tree, harmless unless someone copies it into .git/hooks. Any server-side operation that treats a user-supplied path as a working-tree path but performs it inside a bare repository has collapsed the boundary between user data and Git's control plane.
The Gitea advisory describes precisely this collapse. The diffpatch endpoint applied user patches in a shared bare temporary clone using git apply with --cached, which should only touch the index. Submitting the same patchPatch🛡️A software update that fixes security vulnerabilities, bugs, or adds improvements to an existing program. twice caused an add/add collision, and Git's three-way fallback checked the path out to disk despite --cached. Because the clone was bare, a patch adding an executable file at hooks/post-index-change landed in the real hook directory, and Git ran it the next time it wrote the index, as the Gitea service account.
The Hook Names That MatterMatter🏠A new universal smart home standard backed by Apple, Google, and Amazon for cross-platform compatibility. on a Server
Different hooks fire on different operations, and a server triggers far more of them than a client does. A few are worth knowing by name because they appear in real exploits:
- post-receive and pre-receive run when a push is processed. They are the classic server-side hooks and the ones forge software deliberately exposes as a feature.
- update runs once per ref during a push.
- post-index-change runs after Git writes the index. This is the hook the Gitea exploit abused, chosen because git apply writes the index as part of its normal work, so the attacker did not need to trigger a push at all.
- post-checkout runs after a checkout; anything that checks files out on the server for rendering, diffing, or CI can fire it.
The lesson is that the trigger is not always the obvious one. An attacker looks for whichever Git operation the server performs immediately after their write lands, and picks the hook that fires on it.
Why Forge Software Keeps Hitting This
Source-code hosting platforms perform an enormous number of Git operations on behalf of users: rendering diffs, applying patches from the web editor, merging pull requests, generating archives, running mirrors. Each one is a shell-out to Git, or a library call that behaves like one, against repositories the user controls. Every one of those code paths must guarantee that user-controlled bytes never become an executable file in a hook directory, a config file that sets core.hooksPath or a similar directive, or a symlink pointing at either.
That is a lot of surface, and Git's own behavior is not always intuitive, as the --cached bypass shows. Gitea's 1.27.2 release, published two weeks after the hook fix, closed a separate RCE where a rendering context field allowed argument injection into the markup process. Two different code paths, same root cause: user input reaching a process that runs with the service account's privileges.
Most forges ship with custom hooks disabled for regular users for exactly this reason. In Gitea, the DISABLE_GIT_HOOKS setting defaults to true and governs the feature that lets privileged users author hooks through the UI. That setting protects the intended path. It says nothing about unintended paths like diffpatch, which is why a configuration default cannot substitute for patching.
Defensive Implications
If you operate any Git hosting, self-hosted or embedded in a CI product, a few principles follow directly:
- The service account's reach is the blast radius. Whatever the Gitea, GitLab, or Gogs process can read, an attacker with hook execution can read. Keep application secrets, database credentials, and runner tokens as minimal and as isolated as the deployment allows. Hardening a self-hosted Git server is mostly about shrinking this boundary.
- Write access is the real privilege boundary. Every hook exploit in this class needs a way to write into a repository. Open self-registration hands that privilege to the internet. Treat account creation as an admin function.
- Watch the hooks directories. Executable files appearing in hooks/ under your repository storage, outside of your own provisioning, are a high-fidelity indicator. File-integrity monitoring on that path is cheap and precise.
- Restrict what Git can execute. Running the service in a container with a noexec temporary filesystem removes one of the four preconditions the Gitea advisory lists. It is not a fix, but it breaks the specific chain.
- Patch on the vendor's schedule, not yours. The Gitea fix shipped July 27; the KEV entry came August 25 after real attacks. The month in between is where every exposed instance was a free target.
If an instance was exposed during that window, the right follow-up is a post-compromise checklist for self-hosted services, starting with the assumption that everything the service account could read has been read.