Local Git Runners Using Git Hooks learning to use git hooks means having local continuous integration/delivery published on Friday, 10 July 2026 This text was dumped from starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html with lynx. First, a bit of context. This post was prompted by a question on discourse.32bit.cafe about implementing a Deploy to Neocities Action via Forgejo. The poster was using the git forge provided by 32bit Café to host their website’s git repository, and they wanted the latest version pushed to Neocities whenever they pushed a commit. They couldn’t figure out how to do what they wanted there, so they had wanted to know if they should mirror to GitHub and use GitHub actions. As I told them, they could use git hooks to run arbitrary actions on their local computers. I had also noted that this was something I should do myself. I build my website with GNU make and GNU Emacs, and had been manually running make in my terminal or in Emacs with C-x p c. What follows is how I ended up going about it. I implemented a post-commit and a post-merge hook, because I wanted to build my website locally after committing a change or pulling in changes from the remote repository to my local machine. After I’ve described my implementation, I will explain how to use Git hooks with Neocities CLI. I will not explain how to use make or how to create a makefile, because that is out-of-scope for this tutorial. Nor will I be recording a video or providing screenshots. Neither should be necessary when explaining UNIX shell tools. If you want to try this at home, Occasional Reader, you should possess the following skills: * basic proficiency with UNIX shell commands * basic proficiency with your preferred text editor * basic proficiency with Git You will need the following as well: * a working GNU/Linux system with git installed, or Git for Windows with Git bash installed * an existing git repository for your website * a steady hand * the courage to risk making a mistake step 0: creating a git repository If it helps make you more comfortable, you might want to create an empty repository in which to practice. For example, I am going to create one under ~/projects/website for use in every example I provide. I use zsh as my interactive shell, so your terminal might look different. ~/projects % mkdir website You won’t see anything in your terminal unless something has gone wrong. On UNIX and Linux, no news is good news. Therefore, let’s confirm that the directory exists: ~/projects % cd website You should see something like the following: ~/projects/website % Notice how the shell provides no feedback save that the path in my prompt has changed. Now that we’re in our new project directory, let’s make it an empty git repository using the following command: ~/projects/website % git init I got the following output: Initialized empty Git repository in /home/starbreaker/projects/website/.git/ ~/projects/website % Now that we’ve created a project directory and initiated a git repository, we can continue with the meat of the tutorial. step 1: creating a .githooks directory in your repository While every git repository comes with .git/hooks/ directory full of example scripts, and it is possible to create new scripts in that directory, that is not my preferred approach. The contents of .git/hooks are not version-controlled with the rest of the repository, and seem to remain on the local instance of your repository. Therefore, let’s create a directory that will get version controlled: ~/projects/website % mkdir .githooks We can confirm the new directory’s existence with the following command: ~/projects/website % ls -Al The -A flag will show all hidden files and directories except for . and .., which respectively represent the current and parent directories. The -l will provide the listing on one item per line, with additional detail. These switches and others can be combined, as shown above. The output should resemble this: total 8 drwxrwxr-x 7 starbreaker starbreaker 4096 Jul 10 20:05 .git drwxrwxr-x 2 starbreaker starbreaker 4096 Jul 10 20:44 .githooks ~/projects/website % step 2: telling git to use .githooks Before we create our hook script, let’s first update the git repository’s configuration to look for hooks in .githooks. git provides subcommands for that purpose, as shown below: ~/projects/website % git config core.hooksPath .githooks As usual, the terminal yields no feedback if the command runs successfully. However, you can verify for yourself that your command worked with the UNIX command grep. Here is how I did it: ~/projects/website % grep -n hooks .git/config Running grep -n will print the line number on which any matches appear. This can be prove extremely useful if you find yourself accessing a remote computer, need to alter a configuration file, and the only available text editor is ed(1) (the standard UNIX text editor). The result should resemble the following: 6: hooksPath = .githooks As Stephen King wrote in The Stand as a bioweapons researcher’s last words: "Now you know it works. Any questions?" Check your progress thus far. If you’ve been following along, you should have done the following: 0. created a project directory 1. initialized a git repository 2. created a hidden .githooks directory 3. configured your git repository to look for hooks in .githooks instead of .git/hooks if you aren’t sure, now’s a good time to backtrack. ls and grep are safe to use; they won’t change anything on their own. If it turns out you’ve made a mistake or skipped a step, and you just want to start over, type the following commands: 1. cd ~/projects 2. rm -rf website These will take you out of the website directory, and then nuke it. step 3: implementing a git hook as a bash script Now we come to the fun part. Type nano .githooks/post-commit to open an empty file called post-commit inside the .githooks directory. The following is a sample shell script. I will provide it in its entirety first, and then explain how it works. My explanation will assume that this is the first shell script you’ve ever written. ~/projects/website % cat -n .githooks/post-commit 1 #!/usr/bin/env bash 2 3 # 🄯 2026 Matthew Cambion (matthew.cambion@starbreaker.org) 4 # Available under the GNU General Public License (GPL) v3 5 # 6 # a git post-commit script that does nothing but change directories 7 # and print a silly message for illustrative purposes. 8 9 set -euo pipefail 10 11 CURRENT_DIR=$(pwd) 12 REPO_DIR=$(git rev-parse --show-toplevel) 13 14 cd "${REPO_DIR}" 15 16 echo "It’s a MEWNIX system! My cat knows this! 😺" 17 18 cd "${CURRENT_DIR}" ~/projects/website % source code listing generated with projects/website % cat -n .githooks/post-commit As promised, I will explain this script line by line. This could take some time, Occasional Reader, as I do not wish to assume prior knowledge on your part and thus gloss over a detail that might trip you up. line 1 Line 1 is a requirement for this file to be executable on its own. It’s called a ‘shebang’ in the trade. It allows the script to interpret itself when run from a command line after you’ve made it executable with chmod — which I will explain shortly. The classic UNIX shebang is just #!/bin/sh, but that is a bad idea on modern systems, because /bin/sh is not necessarily the classic Bourne shell from Version 7 UNIX (which was a AT&T Bell Labs project, not Treadstone, if you’re thinking of Jason Bourne). However, it is a bad idea to use the classic shebang on modern systems, because it makes unsafe assumptions about the nature of /bin/sh, which could be an alias for another shell depending on your system. On my system, Debian GNU/Linux 13 (trixie), /bin/sh actually points to dash which is Debian’s variation on the Almquist shell. In the interests of consistency, standard practice when writing shell scripts is to use /usr/bin/env bash to explicitly request the use of the Bourne-again shell, which is the GNU project’s implementation of the Bourne shell. Using #!/usr/bin/env bash tells the operating system to first figure out where bash actually lives, and then use it. This is necessary because not all GNU/Linux distributions place bash in the same directory path. It could live in /bin or /usr/bin. And on BSD systems and macOS, it could live in /usr/local/bin or even /opt/homebrew/bin, since macOS standardized on Zsh to avoid including code licensed under the GNU General Public License. (An explanation of FOSS software licensing politics is outside scope.) lines 3-7 These lines begin with # to indicate to the shell that they are commentary for human readers and not to be interpreted as commands. If you wanted to disable a particular command, you could also place a # in front of it; this is called ‘commenting out’. Not all programming languages use # to indicate a comment, however. line 9 This line sets options for the shell to be as strict as possible when interpreting this script. In the interest of safety, we want the shell to be as pedantic as a typical Hacker News or Reddit commenter. Otherwise, depending on what this script actually does, a failure could have unexpected and potentially catastrophic consequences. And by catastrophic, I mean you’d damned well better have a recent backup, or you’re going to have a really bad time. o -e halts the script on any error o -u halts the script when using an undeclared variable; if this happens, check for typos in variable names first. (I know this from experience.) o -o pipefail isn’t directly relevant here, but I include it because it’s a good habit to maintain when shell scripting. It will ensure that the script fails immediately if any command in a pipeline fails. line 11 This line executes the pwd (print working directory) command and stashes the result in a variable called CURRENT_DIR instead of send it to your terminal. line 12 This line declares and sets a variable called REPO_DIR by using git rev-parse --show-toplevel to get your current project’s root directory. This is handy if you want this script to work in the repository’s main directory, and not fail because it can’t find something in a subdirectory where you had actually run git commit. line 14 This command will have the shell change its working directory to the path stored in $REPO_DIR. When declaring a variable, you do not prefix it with a dollar sign, but this is mandatory when using a variable. Also, because of how shell variable expansion works, it is safest to access a variable as shown in lines 14 and 18. line 16 This line is strictly for illustrative purposes. All it does is print a silly message to the standard output, which is usually your terminal. I used the echo command here because it is perfectly safe by default; it does not change anything on your system. line 18 This is similar to line 16, but uses cd to reset the working directory to where you were when you ran git commit. Now, let’s talk about cat -n, shall we? It isn’t directly relevant to this tutorial, but since I used it I think I should explain it. cat is short for ‘catenate’, and will take whatever file or set of files you specify and dump their contents into your terminal. If cat dumps multiple files, it will combine their contents in sequence. (A careful reader may have observed that -n provides the same functionality for both grep and cat.) Pause here. If all of that seemed like a lot for a script that’s less than 20 lines of code, I don’t blame you! That probably took you at least as long to read as it did for me to type! Now would probably be a good time to stand up, stretch, use the toilet, and get yourself a glass of water and perhaps a small, healthy snack. step 4: testing .githooks/post-commit Feeling better, Occasional Reader? We’re past the hard part, I think. If you’ve typed out the shell script I described in the code listing in step 3, now it’s time to test it. Unless you’ve skipped ahead because you’ve written shell scripts before, this script isn’t actually executable yet. That need not stop us, however. Type the following to test your script without making it executable. ~/projects/website % bash .githooks/post-commit If you haven’t made any mistakes, you should see the following on the next line. It’s a MEWNIX system! My cat knows this! 😺 If you had typed the cat emoji but don’t see it, you might not have a suitable monospace emoji font installed. This is fine; the emoji is just for flavor in this tutorial. If running the script doesn’t yield the result I’ve described, you might want to install the shellcheck tool. It will identify any mistakes you’ve made so that you can fix them. I would also suggest that if you mean to do any shell scripting, then you should install shellcheck and use before running any script for the first time. It should save you some headaches. step 5: making .githooks/post-commit executable Now that we’ve confirmed that this script works, we need to ensure that it can run without our help. First, let‘s take a look at the script’s current permissions with ls -Al .githooks. The results should look somewhat like this: -rw-rw-r-- 1 starbreaker starbreaker 442 Jul 10 23:15 post-commit The -rw-rw-r-- part indicates that for the user who owns this file and their group (both ‘starbreaker’ in my case), this file is readable and writable, but not executable. Anybody who isn’t ‘starbreaker’ doesn’t get to alter this file; they can only view it. The simplest way to make this file executable is to type chmod +x .githooks/post-commit. Afterward, when you run ls -Al .githooks again, you should see the following: -rwxrwxr-x 1 starbreaker starbreaker 442 Jul 10 23:15 post-commit This indicates that the file can now run on its own if you type https://starbreaker.org/.githooks/post-commit. The https://starbreaker.org/ is important when running shell scripts that live in directories not listed in echo "${PATH}", because it’s a shortcut that tells the shell to use the current working directory as a starting point for finding the script you want to run. step 6: adding .githooks/post-commit to the repository and committing the change If you are satisfied that .githooks/post-commit works, now is the time to your git repository and commit it. This typically involves two commands: 1. git add .githooks/post-commit 2. git commit If you’ve followed every step without making any mistakes — and it’s perfectly OK to make mistakes on your party computer! — you should see output similar to the following: It’s a MEWNIX system! My cat knows this! 😺 main (root-commit) 37389c8 this is a test. 1 file changed, 18 insertions(+) create mode 100755 .githooks/post-commit See that silly message? That proves that git ran .githooks/post-commit as soon as it had committed your change. It then dumped the commit details to your terminal on the subsequent lines. This means you’ve successfully implemented a post-commit hook for your git repository. enjoy a victory fanfare: Download audio step 7: using the Neocities CLI in your post-commit script Open up .githooks/post-commit and replace line 16 with neocities push "${DST_DIR}", where "${DST_DIR}" is where the result of your build step goes. Whatever commands you use to actually build your website should go before the invocation of neocities push "${DST_DIR}". In my case, that would be make -j$(nproc), but my makefile also uses rsync to deploy to Nearly Free Speech. Save the file, but don’t commit your changes yet. Instead, manually run .githooks/post-commit first. The Neocities CLI page does not indicate whether you still need to run the neocities CLI manually the first time in order to log in. Once you are satisfied that the updated .githooks/post-commit script works as indended, you can stage and commit your changes. Caveats The post-commit hook only fires when you commit changes. If you want to build or deploy after pulling changes, you might want to use the post-merge hook. If you want to build or deploy after checking out a branch, consider using the post-checkout hook. If you work with your repository on multiple machines, you will have to carry out step 2 on every machine on which you want to run automatic builds and deployments. Some of the location checks and movements aren’t strictly necessary when git runs these scripts, but I’m paranoid. additional reading + 8.3 Customizing Git - Git Hooks + Bash Guide for Beginners by Machtelt Garrels + GNU bash manual starbreaker.org was made with love ❤️‍🔥, defiance 🖕, and Free Software in Tanelorn. The source code and raw text are publicly available on Sourcehut. It is hosted by Nearly Free Speech in the People’s Technocratic Republic of Vinnland. (Vinnland flag designed by Peter Steele of Type O Negative) No LLMs were used to create this website. My consent to use this website as training data for LLMs is hereby denied. starbreaker.org is © 1996-2026 Matthew Thomas Cambion, and is available under Creative Commons BY-NC-SA 4.0. love metal 🤘 — hate fascism 👊 death to rapists and rape culture 💀 trans rights are individual rights 🏳️‍⚧️ antifascist action is not terrorism, but leaderless resistance to state terror 🏴 if a purchase doesn’t confer ownership, then downloading digital media isn’t theft 🏴‍☠️ the United States of America 🇺🇸 was founded in defiance and is thus a Satanic nation 😈 Caveat lector! This is a personal website and thus inherently NSFW. It is likewise unsuitable for unsupervised children under 13 years of age. All opinions published on starbreaker.org are the author’s own unless attributed. They are not representative of his exploiters’ viewpoints or those of their clients and partners. If this website bores or offends you, and you have forgotten how the back button works, dial 1-800-B-DAMNED for technical support. 88x31 button for starbreaker.org starbreaker.org is restricted to adults intertextual violence, crime of literary shock — in nomine meo, amen