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/websitefor use in every example I provide. I usezshas 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
.githooksdirectory 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/hooksare 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
-Aflag will show all hidden files and directories except for . and .., which respectively represent the current and parent directories. The-lwill 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
gitto use.githooks - Before we create our hook script, letâs first update the git repositoryâs configuration to look for hooks
in
.githooks.gitprovides 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 -nwill 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 ised(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:
-
- created a project directory
- initialized a git repository
- created a hidden
.githooksdirectory - configured your git repository to look for hooks in
.githooksinstead of.git/hooks
- if you arenât sure, nowâs a good time to backtrack.
lsandgrepare 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: -
cd ~/projectsrm -rf website
- These will take you out of the website directory, and then nuke it.
- step 3: implementing a
githook as abashscript - Now we come to the fun part. Type
nano .githooks/post-committo open an empty file calledpost-commitinside the.githooksdirectory. - 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/shis 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/shactually points todashwhich 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 bashto explicitly request the use of the Bourne-again shell, which is the GNU projectâs implementation of the Bourne shell. Using#!/usr/bin/env bashtells the operating system to first figure out wherebashactually lives, and then use it. This is necessary because not all GNU/Linux distributions placebashin the same directory path. It could live in/binor/usr/bin. And on BSD systems and macOS, it could live in/usr/local/binor 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.
-
-ehalts the script on any error-uhalts the script when using an undeclared variable; if this happens, check for typos in variable names first. (I know this from experience.)-o pipefailisnâ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 calledCURRENT_DIRinstead of send it to your terminal. - line 12
- This line declares and sets a variable called
REPO_DIRby usinggit rev-parse --show-toplevelto 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 rungit 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
echocommand 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
cdto reset the working directory to where you were when you rangit 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.catis short for âcatenateâ, and will take whatever file or set of files you specify and dump their contents into your terminal. Ifcatdumps multiple files, it will combine their contents in sequence. - (A careful reader may have observed that
-nprovides the same functionality for bothgrepandcat.) - 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
shellchecktool. 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 installshellcheckand use before running any script for the first time. It should save you some headaches. - step 5: making
.githooks/post-commitexecutable - 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 runls -Al .githooksagain, 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
./.githooks/post-commit. The./is important when running shell scripts that live in directories not listed inecho "${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-committo the repository and committing the change - If you are satisfied that
.githooks/post-commitworks, now is the time to your git repository and commit it. - This typically involves two commands:
-
git add .githooks/post-commitgit 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
gitran.githooks/post-commitas 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-commitand replace line 16 withneocities 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 ofneocities push "${DST_DIR}". In my case, that would bemake -j$(nproc), but my makefile also usesrsyncto deploy to Nearly Free Speech. - Save the file, but donât commit your changes yet. Instead, manually run
.githooks/post-commitfirst. 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-commitscript works as indended, you can stage and commit your changes. - Caveats
- The
post-commithook only fires when you commit changes. If you want to build or deploy after pulling changes, you might want to use thepost-mergehook. If you want to build or deploy after checking out a branch, consider using thepost-checkouthook. - 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