<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/css" href="../assets/styles/atom.css"?>
<?xml-stylesheet type="text/css" href="../assets/styles/screen-basic.css"?>
<?xml-stylesheet type="text/css" href="../assets/styles/screen-enhanced.css"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://starbreaker.org">
<title>starbreaker.org: thaumaturgy</title>
<subtitle>full-text, full-stack practical magick: UNIX, Emacs, and webcraft</subtitle>
<link href="https://starbreaker.org/thaumaturgy/feed.xml" rel="self" />
<link href="https://starbreaker.org/thaumaturgy/index.html" rel="alternate" type="text/html" />
<updated>2026-07-11T01:47:12-04:00</updated>
<id>tag:starbreaker.org,2020-05-29:/thaumaturgy/feed.xml</id>
<author>
<name>Matthew Cambion</name>
<email>matthew.cambion@starbreaker.org</email>
</author>
<generator>artisanal HTML, CSS, XML, and local build tooling made with GNU make, sed, m4, bash, and GNU Emacs 30.1 on Debian 13 (trixie)</generator>
<rights>🄯 1996-2026 Matthew Thomas Cambion (Creative Commons BY-NC-SA 4.0 (AI scrapers fuck off))</rights>
<entry>
<title>Local Git Runners Using Git Hooks</title>
<link href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html" rel="alternate" type="text/html" />
<link href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.txt" rel="alternate" type="text/plain" />
<published>2026-07-10T19:05:47-04:00</published>
<updated>2026-07-11T01:47:12-04:00</updated>
<id>tag:starbreaker.org,2020-05-29:/thaumaturgy/local-git-runners-using-git-hooks.html</id>
<summary>learning to use git hooks means having local continuous integration/delivery</summary>
<content xml:lang="en" type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<p> First, a bit of context. This post was prompted by a question on <a href="https://discourse.32bit.cafe/">discourse.32bit.cafe</a> about implementing a <a href="https://discourse.32bit.cafe/t/deploy-to-neocities-action-via-forgejo/4859">Deploy to Neocities Action
via Forgejo</a>. The poster was using the <a href="https://git.32bit.cafe">git forge</a> provided by <a href="https://32bit.cafe">32bit Café</a> to host their website’s git repository, and they wanted the latest
version pushed to <a href="https://neocities.org/">Neocities</a> 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.</p>
<p> As I told them, they could use <a href="https://git-scm.com/docs/githooks">git hooks</a> to run arbitrary
actions on their local computers. I had also noted that this was something I should do <em>myself</em>. I
build my website with <a href="https://www.gnu.org/software/make/" title="Make - GNU Project - Free Software Foundation">GNU make</a> and <a href="https://www.gnu.org/s/emacs/" title="GNU Emacs - GNU Project - Free Software Foundation">GNU Emacs</a>, and had been manually running make
in my terminal or in Emacs with <kbd>C-x p c</kbd>.</p>
<p> What follows is how I ended up going about it. I implemented a <code>post-commit</code> and a <code>post-merge</code> 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 <a href="https://neocities.org/cli" title="Neocities - Command Line Interface">Neocities CLI</a>.</p>
<p> I will not explain how to use <code>make</code> 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.</p>
<p> If you want to try this at home, Occasional Reader, you should possess the following skills:</p>
<ul><li>basic proficiency with UNIX shell commands</li><li>basic proficiency with your preferred text editor</li><li>basic proficiency with <a href="https://git-scm.com/">Git</a></li></ul>
<p> You will need the following as well:</p>
<ul><li>a working GNU/Linux system with git installed, or <a href="https://git-scm.com/install/windows">Git for
Windows with Git bash</a> installed</li><li>an existing git repository for your website</li><li>a steady hand</li><li>the courage to risk making a mistake</li></ul>
<dl><dt id="step0">step 0: creating a git repository</dt><dd> 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 <code>~/projects/website</code> for use in every example I
provide. I use <code>zsh</code> as my interactive shell, so your terminal might look different.</dd><dd><code>~/projects % mkdir website</code></dd><dd> 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:</dd><dd><code>~/projects % cd website</code></dd><dd>You should see something like the following:</dd><dd><samp>~/projects/website % </samp></dd><dd> 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:</dd><dd><code>~/projects/website % git init</code></dd><dd>I got the following output:</dd><dd>
<pre><samp>Initialized empty Git repository in /home/starbreaker/projects/website/.git/
~/projects/website % </samp></pre>
</dd><dd>Now that we’ve created a project directory and initiated a git repository, we can continue with the meat
of the tutorial.</dd><dt id="step1">step 1: creating a <code>.githooks</code> directory in your repository</dt><dd> While every git repository comes with <code>.git/hooks/</code> directory full of example scripts, and
it is possible to create new scripts in that directory, that is not my preferred approach.</dd><dd> The contents of <code>.git/hooks</code> 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 <em>will</em> get version controlled:</dd><dd><code>~/projects/website % mkdir .githooks</code></dd><dd>We can confirm the new directory’s existence with the following command:</dd><dd><code>~/projects/website % ls -Al</code></dd><dd> The <code>-A</code> flag will show all hidden files and directories except for <samp>.</samp> and
<samp>..</samp>, which respectively represent the current and parent directories. The <code>-l</code>
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:</dd><dd>
<pre><samp>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 % </samp></pre>
</dd><dt id="step2">step 2: telling <code>git</code> to use <code>.githooks</code></dt><dd> Before we create our hook script, let’s first update the git repository’s configuration to look for hooks
in <code>.githooks</code>. <code>git</code> provides subcommands for that purpose, as shown below:</dd><dd><code>~/projects/website % git config core.hooksPath .githooks</code></dd><dd> 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 <code>grep</code>. Here is how I did it:</dd><dd><code>~/projects/website % grep -n hooks .git/config</code></dd><dd> Running <code>grep -n</code> 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 <code>ed(1)</code> (the standard UNIX text editor). The result should
resemble the following:</dd><dd><samp>6: hooksPath = .githooks</samp></dd><dd> As Stephen King wrote in <cite data-type="novel">The Stand</cite> as a bioweapons researcher’s last
words: <q>Now you know it works. Any questions?</q></dd><dt id="progress-check">Check your progress thus far.</dt><dd> If you’ve been following along, you should have done the following:</dd><dd>
<ol start="0"><li>created a project directory</li><li>initialized a git repository</li><li>created a hidden <code>.githooks</code> directory</li><li>configured your git repository to look for hooks in <code>.githooks</code> instead of <code>.git/hooks</code></li></ol>
</dd><dd> if you aren’t sure, now’s a good time to backtrack. <code>ls</code> and <code>grep</code> 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:</dd><dd>
<ol><li><code>cd ~/projects</code></li><li><code>rm -rf website</code></li></ol>
</dd><dd> These will take you out of the website directory, and then nuke it.</dd><dt id="step3">step 3: implementing a <code>git</code> hook as a <code>bash</code> script</dt><dd> Now we come to the fun part. Type <code>nano .githooks/post-commit</code> to open an empty file called
<code>post-commit</code> inside the <code>.githooks</code> directory.</dd><dd> 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.</dd><dd>
<figure id="post-commit-listing">
<pre><samp>~/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 % </samp></pre>
<figcaption>source code listing generated with <code>projects/website % cat -n
.githooks/post-commit</code></figcaption>
</figure>
</dd><dd> 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.</dd><dd>
<dl><dt id="step3-line1">line 1</dt><dd> 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 <code>chmod</code> — which I will explain shortly.</dd><dd> The classic UNIX shebang is just <code>#!/bin/sh</code>, but that is a bad idea on modern systems,
because <code>/bin/sh</code> is not necessarily the <a href="https://en.wikipedia.org/wiki/Bourne_shell" title="Bourne shell - Wikipedia">classic Bourne
shell</a> from Version 7 UNIX (which was a AT&amp;amp;T Bell Labs project, not Treadstone, if you’re
thinking of <em>Jason</em> Bourne).</dd><dd> However, it is a bad idea to use the classic shebang on modern systems, because it makes unsafe
assumptions about the nature of <code>/bin/sh</code>, which could be an alias for another shell
depending on your system. On my system, Debian GNU/Linux 13 (trixie), <code>/bin/sh</code> actually
points to <code>dash</code> which is Debian’s variation on the <a href="https://en.wikipedia.org/wiki/Almquist_shell" title="Almquist shell - Wikipedia">Almquist shell</a>.</dd><dd> In the interests of consistency, standard practice when writing shell scripts is to use <code>/usr/bin/env bash</code> to explicitly request the use of the <a href="https://en.wikipedia.org/wiki/Bash_(Unix_shell)" title="Bourne-again shell - Wikipedia">Bourne-again shell</a>, which is the <a href="https://gnu.org" title="GNU’s not UNIX">GNU project</a>’s implementation of the Bourne shell. Using <code>#!/usr/bin/env bash</code> tells the operating
system to first figure out where <code>bash</code> actually lives, and then use it. This is necessary
because not all GNU/Linux distributions place <code>bash</code> in the same directory path. It could
live in <code>/bin</code> or <code>/usr/bin</code>. And on BSD systems and macOS, it could live in
<code>/usr/local/bin</code> or even <code>/opt/homebrew/bin</code>, since macOS standardized on <a href="https://zsh.org/">Zsh</a> to avoid including code licensed under the GNU General Public License.</dd><dd>(An explanation of FOSS software licensing politics is outside scope.)</dd><dt id="step3-lines3-7">lines 3-7</dt><dd> These lines begin with <kbd>#</kbd> 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 <kbd>#</kbd> in front of it; this is called ‘commenting out’. Not all programming
languages use <kbd>#</kbd> to indicate a comment, however.</dd><dt id="step3-line9">line 9</dt><dd> 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 <em>better</em> have a recent
backup, or you’re going to have a <em>really</em> bad time.</dd><dd>
<ul><li><code>-e</code> halts the script on any error</li><li><code>-u</code> halts the script when using an undeclared variable; if this happens, check for
typos in variable names first. (I know <em>this</em> from experience.)</li><li><code>-o pipefail</code> 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.</li></ul>
</dd><dt id="step3-line11">line 11</dt><dd>This line executes the <code>pwd</code> (print working directory) command and stashes the result in
a variable called <code>CURRENT_DIR</code> instead of send it to your terminal.</dd><dt id="step3-line12">line 12</dt><dd> This line declares and sets a variable called <code>REPO_DIR</code> by using <code>git rev-parse
--show-toplevel</code> 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 <code>git commit</code>.</dd><dt id="step3-line14">line 14</dt><dd> This command will have the shell change its working directory to the path stored in <code>$REPO_DIR</code>. When declaring a variable, you do not prefix it with a dollar sign, but this is
mandatory when <em>using</em> a variable. Also, because of how shell variable expansion works, it is
safest to access a variable as shown in lines 14 and 18.</dd><dt id="step3-line16">line 16</dt><dd> 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 <code>echo</code> command here because it is
perfectly safe by default; it does not change anything on your system.</dd><dt id="step3-line18">line 18</dt><dd>This is similar to line 16, but uses <code>cd</code> to reset the working directory to where you
were when you ran <code>git commit</code>.</dd></dl>
</dd><dd> Now, let’s talk about <code>cat -n</code>, shall we? It isn’t directly relevant to this tutorial, but
since I used it I think I should explain it. <code>cat</code> is short for ‘catenate’, and will take
whatever file or set of files you specify and dump their contents into your terminal. If <code>cat</code>
dumps multiple files, it will combine their contents in sequence.</dd><dd>(A careful reader may have observed that <code>-n</code> provides the same functionality for both <code>grep</code> and <code>cat</code>.)</dd><dt id="pause-here">Pause here.</dt><dd> 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.</dd><dt id="step4">step 4: testing <code>.githooks/post-commit</code></dt><dd> Feeling better, Occasional Reader? We’re past the hard part, I think.</dd><dd> If you’ve typed out the shell script I described in the <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html#post-commit-listing">code listing</a> in <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html#step3">step 3</a>, 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 <em>without</em>
making it executable.</dd><dd><code>~/projects/website % bash .githooks/post-commit</code></dd><dd>If you haven’t made any mistakes, you should see the following on the next line.</dd><dd><samp>It’s a MEWNIX system! My cat knows this! 😺</samp></dd><dd> 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.</dd><dd> If running the script doesn’t yield the result I’ve described, you might want to install the <code>shellcheck</code> 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 <code>shellcheck</code> and use
before running any script for the first time. It should save you some headaches.</dd><dt id="step5">step 5: making <code>.githooks/post-commit</code> executable</dt><dd> 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 <code>ls -Al .githooks</code>. The
results should look somewhat like this:</dd><dd> <samp>-rw-rw-r-- 1 starbreaker starbreaker 442 Jul 10 23:15 post-commit</samp></dd><dd>The <samp>-rw-rw-r--</samp> part indicates that for the user who owns this file and their group (both
‘starbreaker’ in my case), this file is <em>readable</em> and <em>writable</em>, but not <em>executable</em>. Anybody who isn’t ‘starbreaker’ doesn’t get to alter this file; they can only view it.</dd><dd> The simplest way to make this file executable is to type <code>chmod +x .githooks/post-commit</code>.
Afterward, when you run <code>ls -Al .githooks</code> again, you should see the following:</dd><dd><samp>-rwxrwxr-x 1 starbreaker starbreaker 442 Jul 10 23:15 post-commit</samp></dd><dd> This indicates that the file can now run on its own if you type <code>https://starbreaker.org/.githooks/post-commit</code>.
The <code>https://starbreaker.org/</code> is important when running shell scripts that live in directories not listed in <code>echo "$PATH"</code>, 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.</dd><dt id="step6">step 6: adding <code>.githooks/post-commit</code> to the repository and committing the change</dt><dd>If you are satisfied that <code>.githooks/post-commit</code> works, now is the time to your git
repository and commit it.</dd><dd>This typically involves two commands:</dd><dd>
<ol><li><code>git add .githooks/post-commit</code></li><li><code>git commit</code></li></ol>
</dd><dd>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:</dd><dd>
<pre><samp>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</samp></pre>
</dd><dd> See that silly message? That proves that <code>git</code> ran <code>.githooks/post-commit</code> as
soon as it had committed your change. It then dumped the commit details to your terminal on the subsequent
lines.</dd><dd> This means you’ve successfully implemented a post-commit hook for your git repository.
<figure>
<figcaption>enjoy a victory fanfare:</figcaption>
<audio controls="true" src="https://starbreaker.org/assets/audio/ff1-victory-fanfare.ogg" title="Victory fanfare by Nobuo Uematsu from the original Final Fantasy © 1987 Square Enix (used without permission))"></audio><br></br>
<a href="https://starbreaker.org/assets/audio/ff1-victory-fanfare.ogg">Download audio</a></figure>
</dd><dt id="step7">step 7: using the Neocities CLI in your post-commit script</dt><dd> Open up <code>.githooks/post-commit</code> and replace line 16 with <code>neocities push
"$DST_DIR"</code>, where <code>"$DST_DIR"</code> is where the result of your build step goes. Whatever
commands you use to actually build your website should go before the invocation of <code>neocities push
"$DST_DIR"</code>. In my case, that would be <code>make -j$(nproc)</code>, but my makefile also uses
<code>rsync</code> to deploy to <a href="https://www.nearlyfreespeech.net/" title="reasonably priced web hosting if you speak UNIX">Nearly Free Speech</a>.</dd><dd> Save the file, but don’t commit your changes yet. Instead, manually run <code>.githooks/post-commit</code> first. The <a href="https://neocities.org/cli" title="Neocities - Command Line Interface">Neocities CLI</a> page does not indicate whether you still need
to run the neocities CLI manually the first time in order to log in.</dd><dd>Once you are satisfied that the updated <code>.githooks/post-commit</code> script works as indended, you
can stage and commit your changes.</dd><dt id="caveats">Caveats</dt><dd> The <code>post-commit</code> hook only fires when you commit changes. If you want to build or deploy
after pulling changes, you might want to use the <code>post-merge</code> hook. If you want to build or
deploy after checking out a branch, consider using the <code>post-checkout</code> hook.</dd><dd> If you work with your repository on multiple machines, you will have to carry out <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html#step2">step 2</a> on every machine on which you
want to run automatic builds and deployments.</dd><dd>Some of the location checks and movements aren’t strictly necessary when git runs these scripts, but I’m
paranoid.</dd><dt id="additional-reading">additional reading</dt><dd>
<ul><li><a href="https://git-scm.com/book/ms/v2/Customizing-Git-Git-Hooks" title="Git - Git Hooks"><cite class="straight" data-type="documentation">8.3 Customizing Git - Git Hooks</cite></a></li><li><a href="https://tldp.org/LDP/Bash-Beginners-Guide/html/" title="part of the Linux Documentation Project"><cite class="straight" data-type="documentation">Bash
Guide for Beginners</cite> by Machtelt Garrels</a></li><li><a href="https://www.gnu.org/software/bash/manual/" title="GNU bash manual - GNU project"><cite class="straight" data-type="documentation">GNU bash manual</cite></a></li></ul>
</dd></dl>
<hr class="medium" />
<footer>
<p><strong>Thank you for using <a href="https://aboutfeeds.com/">web feeds</a> to keep up with this website!</strong><br />
If you’re viewing this feed in your browser, here are the <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html" title="rich text version of “Local Git Runners Using Git Hooks”">HTML</a> and <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.txt" title="plain text version of “Local Git Runners Using Git Hooks”">plain text</a> versions of this entry.<br />
If you’d like to get in touch, please <a href="mailto:matthew.cambion@starbreaker.org?subject=RE:%20Local Git Runners Using Git Hooks">reply by email</a>. If you want to communicate more privately, you can reach me via <a href="https://signal.org">Signal</a> by texting <a href="https://signal.me/#eu/rLUQHAt6iG5v_5Pee2BbSON_aa1t88FhO6GgpJfS_ROOyq43F9NHXJAreegQLw_j">starbreaker.84</a>.</p>
</footer>
</div>
</content>
</entry>
</feed>
