<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>dustinkaiser.eu</title><link href="https://dustinkaiser.eu/" rel="alternate"/><link href="https://dustinkaiser.eu/feeds/all.atom.xml" rel="self"/><id>https://dustinkaiser.eu/</id><updated>2026-05-07T00:00:00-04:00</updated><subtitle>Dustin Kaiser</subtitle><entry><title>How to not shoot yourself in the foot on production</title><link href="https://dustinkaiser.eu/blog/2026/how-to-not-shoot-yourself-in-the-foot-on-production/" rel="alternate"/><published>2026-05-07T00:00:00-04:00</published><updated>2026-05-07T00:00:00-04:00</updated><author><name>Dustin Kaiser</name></author><id>tag:dustinkaiser.eu,2026-05-07:/blog/2026/how-to-not-shoot-yourself-in-the-foot-on-production/</id><summary type="html">&lt;p&gt;Using the bash DEBUG trap to stop yourself from running dangerous commands on production systems — and why none of the more obvious approaches actually work.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I work in a small, fast-paced team at a custom-research company. We build and operate a fairly involved platform, and there are maybe a handful of us who regularly touch production infrastructure. We can't always afford the organizational overhead to justify a full-blown change-management process with four-eyes policies for every command typed on a server. Sometimes, at 11pm on a Thursday, you need to SSH into a production node and figure out why a Docker service is misbehaving. That is the reality, well, my current reality at least...&lt;/p&gt;
&lt;p&gt;But the reality also includes the fact that tired or multitasking engineers type dangerous things. &lt;code&gt;docker volume prune&lt;/code&gt; on a node with persistent data. &lt;code&gt;docker swarm leave&lt;/code&gt; on a manager. &lt;code&gt;apt upgrade&lt;/code&gt; on a system that should only get targeted patches. These are the kinds of commands that, once executed, tend to ruin your evening in painful ways.&lt;/p&gt;
&lt;p&gt;What we wanted was something low-ceremony: a tripwire. Something that makes you stop and think for a second before you proceed, and that takes about five seconds to override when you actually know what you are doing. The poor man's approach to not shooting yourself in the foot.&lt;/p&gt;
&lt;p&gt;It turns out bash has a fairly unknown mechanism that is almost perfectly suited for this&lt;sup id="fnref:rbash"&gt;&lt;a class="footnote-ref" href="#fn:rbash"&gt;1&lt;/a&gt;&lt;/sup&gt;. It lives in a corner of the shell that was designed for debuggers, and most people have never heard of it. Full disclosure: I got this from a not-very-much-upvoted stackoverflow post: https://stackoverflow.com/a/55977897 . Thank you, user xhienne!&lt;/p&gt;
&lt;h2 id="the-debug-trap"&gt;The DEBUG trap&lt;a class="headerlink" href="#the-debug-trap" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Bash's &lt;code&gt;trap&lt;/code&gt; builtin is most commonly used with signals — &lt;code&gt;trap cleanup EXIT&lt;/code&gt;, &lt;code&gt;trap '' INT&lt;/code&gt;, that sort of thing. But bash also supports several pseudo-signals, and the one that interests us here is &lt;code&gt;DEBUG&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;DEBUG&lt;/code&gt; trap fires &lt;em&gt;before&lt;/em&gt; every simple command, &lt;code&gt;for&lt;/code&gt; command, &lt;code&gt;case&lt;/code&gt; command, &lt;code&gt;select&lt;/code&gt; command, arithmetic command, conditional command, and before the first command in a shell function. The variable &lt;code&gt;$BASH_COMMAND&lt;/code&gt; holds the full text of the command that is about to be executed. This by itself is already interesting: you get to inspect what the user typed before the shell does anything with it.&lt;/p&gt;
&lt;p&gt;But the real trick stems from two additional settings that together render the &lt;code&gt;DEBUG&lt;/code&gt; trap into an actual interception mechanism:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;shopt -s extdebug&lt;/code&gt;&lt;/strong&gt; enables extended debugging mode. Among other things, this changes the behavior of the &lt;code&gt;DEBUG&lt;/code&gt; trap: if the trap handler returns a non-zero exit status, &lt;em&gt;the next command is skipped and not executed.&lt;/em&gt; This is the critical piece. Without &lt;code&gt;extdebug&lt;/code&gt;, the &lt;code&gt;DEBUG&lt;/code&gt; trap is purely observational — it can log, but it cannot prevent. With &lt;code&gt;extdebug&lt;/code&gt;, a non-zero return from the trap handler tells bash to silently discard the pending command.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;set -T&lt;/code&gt;&lt;/strong&gt; enables function tracing. Normally, the &lt;code&gt;DEBUG&lt;/code&gt; trap is not inherited by shell functions or subshells. &lt;code&gt;set -T&lt;/code&gt; ensures that the trap propagates into functions, command substitutions, and subshells invoked with &lt;code&gt;(command)&lt;/code&gt;. Without this, a user could sidestep the trap.&lt;/p&gt;
&lt;p&gt;The combination of these three pieces — &lt;code&gt;trap '...' DEBUG&lt;/code&gt;, &lt;code&gt;shopt -s extdebug&lt;/code&gt;, and &lt;code&gt;set -T&lt;/code&gt; — yields something that none of the other approaches can provide: a hook that fires before every command, has access to the command text, and can prevent execution by returning non-zero.&lt;/p&gt;
&lt;h2 id="putting-it-together"&gt;Putting it together&lt;a class="headerlink" href="#putting-it-together" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The implementation is straightforward. In &lt;code&gt;.bashrc&lt;/code&gt;, we install a &lt;code&gt;DEBUG&lt;/code&gt; trap that pattern-matches &lt;code&gt;$BASH_COMMAND&lt;/code&gt; against a list of blocked command prefixes. If a match is found, the trap handler prints a warning and returns &lt;code&gt;false&lt;/code&gt;, which causes bash to skip the command. If no match is found, the handler returns &lt;code&gt;true&lt;/code&gt;, and execution proceeds normally.&lt;/p&gt;
&lt;p&gt;The blocked commands we use include things like &lt;code&gt;docker volume prune&lt;/code&gt;, &lt;code&gt;docker swarm leave&lt;/code&gt;, &lt;code&gt;docker stack rm&lt;/code&gt;, &lt;code&gt;docker service rm&lt;/code&gt;, and &lt;code&gt;apt upgrade&lt;/code&gt;. These are the commands that, in our context, are most often typed either by accident or without sufficient thought about the consequences on a particular node.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;trap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;  if [[ &amp;quot;$BASH_COMMAND&amp;quot; == &amp;quot;docker volume prune&amp;quot;* ]]; then&lt;/span&gt;
&lt;span class="s1"&gt;    printf &amp;quot;[Forbidden: &amp;gt;&amp;gt; %s &amp;lt;&amp;lt; Run \&amp;quot;trap - DEBUG\&amp;quot; to allow.]\n&amp;quot; &amp;quot;$BASH_COMMAND&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    false&lt;/span&gt;
&lt;span class="s1"&gt;  elif [[ &amp;quot;$BASH_COMMAND&amp;quot; == &amp;quot;docker swarm leave&amp;quot;* ]]; then&lt;/span&gt;
&lt;span class="s1"&gt;    printf &amp;quot;[Forbidden: &amp;gt;&amp;gt; %s &amp;lt;&amp;lt; Run \&amp;quot;trap - DEBUG\&amp;quot; to allow.]\n&amp;quot; &amp;quot;$BASH_COMMAND&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    false&lt;/span&gt;
&lt;span class="s1"&gt;  elif [[ &amp;quot;$BASH_COMMAND&amp;quot; == &amp;quot;docker stack rm&amp;quot;* ]]; then&lt;/span&gt;
&lt;span class="s1"&gt;    printf &amp;quot;[Forbidden: &amp;gt;&amp;gt; %s &amp;lt;&amp;lt; Run \&amp;quot;trap - DEBUG\&amp;quot; to allow.]\n&amp;quot; &amp;quot;$BASH_COMMAND&amp;quot;&lt;/span&gt;
&lt;span class="s1"&gt;    false&lt;/span&gt;
&lt;span class="s1"&gt;  fi&lt;/span&gt;
&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;DEBUG
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-T
&lt;span class="nb"&gt;shopt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;extdebug
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When you type a blocked command, you see something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[Forbidden: &amp;gt;&amp;gt; docker stack rm mystack &amp;lt;&amp;lt; Run &amp;quot;trap - DEBUG&amp;quot; to allow.]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The command is not executed. Your swarm stack is still there.&lt;/p&gt;
&lt;h2 id="the-escape-hatch"&gt;The escape hatch&lt;a class="headerlink" href="#the-escape-hatch" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is not a security boundary, and it is not intended to be one. It is a tripwire. If you actually need to run one of the blocked commands — and this happens more often than one thinks — you clear the trap:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;trap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;DEBUG
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This removes the &lt;code&gt;DEBUG&lt;/code&gt; trap entirely, for the current shell session. You run your command. Once you exit the (e.g. ssh) shell session, the .bashrc will be sourced again, and your trap is active again Or, you re-source &lt;code&gt;.bashrc&lt;/code&gt; to put the tripwire back:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.bashrc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We deploy this via Ansible to pretty much all "important production" as well as "someone's playground" servers, injected into &lt;code&gt;.bashrc&lt;/code&gt; for both the &lt;code&gt;ubuntu&lt;/code&gt; and &lt;code&gt;root&lt;/code&gt; users. The blocked command list lives in a separate text file, one command prefix per line, which makes it easy to update. For anyone who wants to deploy this across their machines, here is the Ansible snippet I use. It reads a &lt;code&gt;blocked_commands.txt&lt;/code&gt; file (one command prefix per line), and there is some jinja magic in there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Read blocked commands from local file&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;set_fact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;blocked_cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lookup(&amp;#39;file&amp;#39;,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;blocked_commands.txt&amp;#39;).splitlines()&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&amp;quot;&lt;/span&gt;

&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Construct trap command&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;set_fact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;trap_cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;&amp;gt;-&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;{% if trap_cmd is defined %}{{ trap_cmd }}{% else %}if [[ &amp;quot;$BASH_COMMAND&amp;quot; == &amp;quot;{{ item }}&amp;quot;* ]]; then printf &amp;quot;[Forbidden: &amp;gt;&amp;gt; %s &amp;lt;&amp;lt; This might lead to issues. Run \&amp;quot;trap - DEBUG\&amp;quot; to allow command. source ~/.bashrc to disable it again]\n&amp;quot; &amp;quot;$BASH_COMMAND&amp;quot;; false; {% endif %}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;elif [[ &amp;quot;$BASH_COMMAND&amp;quot; == &amp;quot;{{ item }}&amp;quot;* ]]; then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;printf &amp;quot;[Forbidden: &amp;gt;&amp;gt; %s &amp;lt;&amp;lt; This might lead to issues. Run \&amp;quot;trap - DEBUG\&amp;quot; to allow command. source ~/.bashrc to disable it again]\n&amp;quot; &amp;quot;$BASH_COMMAND&amp;quot;; false;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;blocked_cmds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&amp;quot;&lt;/span&gt;

&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Apply trap to .bashrc for user ubuntu&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;blockinfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/home/ubuntu/.bashrc&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;# via https://stackoverflow.com/a/55977897&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;trap &amp;#39;{{ trap_cmd + &amp;quot;fi;&amp;quot; | trim }}&amp;#39; DEBUG&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;set -T&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;shopt -s extdebug&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;#&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{mark}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ANSIBLE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MANAGED&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;BLOCK&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;restriction&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;via&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trap&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;insertbefore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;EOF&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="colored-shell-prompts"&gt;Colored shell prompts&lt;a class="headerlink" href="#colored-shell-prompts" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;While we are at it: Another cheap &lt;code&gt;.bashrc&lt;/code&gt; trick that pairs well with the command tripwire: making it visually obvious &lt;em&gt;where&lt;/em&gt; you are.  If you spend your day hopping between terminals it is surprisingly easy to lose track of which shell is connected to which environment. Especially when you have four terminal tabs open and they all say &lt;code&gt;ubuntu@ip-10-0-something&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We tackle this by overwriting &lt;code&gt;PS1&lt;/code&gt; per deployment stage, with color-coded &lt;code&gt;[STAGE]&lt;/code&gt; suffixes. This trick is a bit more common, I got it from Mitchel Hashimoto in some youtube video but since then I've seen it around. The prompt on a production node is red and says &lt;code&gt;[PROD]&lt;/code&gt;. Staging is orange, &lt;code&gt;[STAGING]&lt;/code&gt;. But you can do whatever in practice. In any case, this is the kind of thing that costs nothing to set up and saves your behind the moment your muscle memory tries to run a prod command in what you thought was a staging session. Here is some example for debian based systems:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;set vars for shell prompt&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;set_fact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;stage_colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# green&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;master&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;\[\033[01;32m\]&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# orange&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;\[\e[38;5;202;1m\]&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# red&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;\[\e[38;5;160;1m\]&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;green_color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;\[\033[01;32m\]&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;reset_formatting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;\[\033[00m\]&amp;#39;&lt;/span&gt;

&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Update shell prompt&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;blockinfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/home/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/.bashrc&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;#&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{mark}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ANSIBLE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MANAGED&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;BLOCK&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shell&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PS1&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;insertbefore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;unset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;color_prompt&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;force_color_prompt&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;if [ &amp;quot;$color_prompt&amp;quot; = yes ]; then&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;PS1=&amp;#39;${debian_chroot:+($debian_chroot)}{{ green_color }}\u@\h{{ reset_formatting }}:\w{{ stage_colors[deployment_stage] }}[{{ deployment_stage | upper }}]\${{ reset_formatting }} &amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;PS1=&amp;#39;${debian_chroot:+($debian_chroot)}\u@\h:\w[{{ deployment_stage | upper }}]\$ &amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The result is that on a prod machine your prompt reads something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="nv"&gt;@ip&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~[&lt;/span&gt;&lt;span class="n"&gt;PROD&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...in red.&lt;/p&gt;
&lt;p&gt;This is how I stop myself from shooting myself in the foot - on prod.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:rbash"&gt;
&lt;p&gt;You might think of bash's restricted mode (&lt;code&gt;rbash&lt;/code&gt;), which disables things like changing directories, setting &lt;code&gt;PATH&lt;/code&gt;, and running commands with slashes. This is far too blunt an instrument. We are not trying to lock people out of the system — we are trying to prevent a specific class of mistakes while leaving everything else fully operational. A restricted shell renders the machine nearly unusable for the kind of debugging and administration work that necessitates SSH access in the first place.&amp;#160;&lt;a class="footnote-backref" href="#fnref:rbash" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="Science"/><category term="bash"/><category term="linux"/><category term="infrastructure"/><category term="devops"/></entry><entry><title>Managing dotfiles with GNU Stow</title><link href="https://dustinkaiser.eu/blog/2026/managing-dotfiles-with-gnu-stow/" rel="alternate"/><published>2026-05-07T00:00:00-04:00</published><updated>2026-05-07T00:00:00-04:00</updated><author><name>Dustin Kaiser</name></author><id>tag:dustinkaiser.eu,2026-05-07:/blog/2026/managing-dotfiles-with-gnu-stow/</id><summary type="html">&lt;p&gt;Using GNU Stow to version-control and synchronize dotfiles across machines.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Everyone who works on more than one Linux machine eventually faces the dotfiles problem. You tweak your shell prompt on your laptop, add a git alias on your workstation, and within a few weeks the configurations have drifted apart. It's annoying and difficult to reconcile. I have seen people commit the entire home directory to git to address this — it has fairly obvious drawbacks.&lt;/p&gt;
&lt;p&gt;The approach I currently use is &lt;a href="https://www.gnu.org/software/stow/"&gt;GNU Stow&lt;/a&gt;, a symlink manager that was originally designed for managing software installed from source into &lt;code&gt;/usr/local&lt;/code&gt; - or so I am told. It turns out to be an unreasonably good fit for dotfiles.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;a class="headerlink" href="#how-it-works" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Stow operates on a simple idea: you organize files in a directory tree that mirrors the structure of your home directory, and stow creates the corresponding symlinks for you. The dotfiles repo lives at &lt;code&gt;~/.dotfiles&lt;/code&gt;, and inside it each "package" is a subdirectory whose contents reflect the target layout relative to &lt;code&gt;$HOME&lt;/code&gt;. For instance, the &lt;code&gt;git&lt;/code&gt; package contains &lt;code&gt;.gitconfig&lt;/code&gt;, the &lt;code&gt;ghostty&lt;/code&gt; package contains &lt;code&gt;.config/ghostty/config&lt;/code&gt;, and so on. The directory structure inside each package is the path structure you want relative to your home directory.https://spencer.wtf/2026/02/20/cleaning-up-merged-git-branches-a-one-liner-from-the-cias-leaked-dev-docs.html&lt;/p&gt;
&lt;p&gt;Deploying everything is a single command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;stow&lt;span class="w"&gt; &lt;/span&gt;--verbose&lt;span class="w"&gt; &lt;/span&gt;--target&lt;span class="o"&gt;=&lt;/span&gt;/home/dustin/&lt;span class="w"&gt; &lt;/span&gt;--restow&lt;span class="w"&gt; &lt;/span&gt;*/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--restow&lt;/code&gt; flag first unstows and then re-stows each package, which cleans up any stale symlinks from files you may have removed. The &lt;code&gt;*/&lt;/code&gt; glob expands to all subdirectories — i.e., all packages. That is it. I usually dig this command out of my bash_history when I need it.&lt;/p&gt;
&lt;p&gt;When I change a config on any machine, I edit the file in &lt;code&gt;~/.dotfiles&lt;/code&gt;, commit, push, and pull on the other machines. Since stow created symlinks, there is no copy step.&lt;/p&gt;
&lt;h2 id="what-i-manage-with-it"&gt;What I manage with it&lt;a class="headerlink" href="#what-i-manage-with-it" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My repo - that I keep on a git forge on my private home server - currently has around 25 packages. Some of the more interesting ones:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terminal and shell.&lt;/strong&gt; Ghostty terminal config, Starship prompt and Atuin for shell history sync across machines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Git.&lt;/strong&gt; A &lt;code&gt;.gitconfig&lt;/code&gt; with aliases I have accumulated over the years. &lt;a href="https://spencer.wtf/2026/02/20/cleaning-up-merged-git-branches-a-one-liner-from-the-cias-leaked-dev-docs.html"&gt;ciaclean&lt;/a&gt; anyone?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editor configs.&lt;/strong&gt; Neovim config and VS Code settings — including MCP server configs and custom prompt files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Custom scripts.&lt;/strong&gt; A &lt;code&gt;bin/&lt;/code&gt; package that lands a collection of small utilities into &lt;code&gt;~/bin&lt;/code&gt;: &lt;code&gt;csv2md&lt;/code&gt; for quick CSV-to-markdown-table conversion, &lt;code&gt;sqcat&lt;/code&gt; to print .sqlite files to my terminal, and some other fun ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI tooling.&lt;/strong&gt; GitHub Copilot skills (writing style guides, project management workflows, obtaining YouTube transcripts...). These live in &lt;code&gt;.copilot/skills/&lt;/code&gt; and get symlinked into place on every machine, so the AI assistant behaves the same way everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Systemd user services.&lt;/strong&gt; Unit files for things like Kanshi (dynamic display configuration for Wayland), LiteLLM (a local LLM proxy), and Ollama.&lt;/p&gt;</content><category term="Science"/><category term="linux"/><category term="dotfiles"/><category term="git"/><category term="stow"/><category term="devops"/></entry><entry><title>Welcome</title><link href="https://dustinkaiser.eu/blog/2026/welcome/" rel="alternate"/><published>2026-04-14T00:00:00-04:00</published><updated>2026-04-14T00:00:00-04:00</updated><author><name>Dustin Kaiser</name></author><id>tag:dustinkaiser.eu,2026-04-14:/blog/2026/welcome/</id><summary type="html">&lt;p&gt;First post — what this site is about and what to expect.&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is my place for writing about science, code, and the
ideas that connect them.&lt;/p&gt;
&lt;p&gt;I've been working at the intersection of computational science and
software engineering for a while, and I've accumulated enough stray
ideas that they deserve a place to live. Some posts here will be
technical deep dives. Others will be shorter notes on tools, workflows,
or unsolicited rantings&lt;/p&gt;
&lt;h2 id="what-to-expect"&gt;What to expect&lt;a class="headerlink" href="#what-to-expect" title="Permanent link"&gt;&amp;para;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Science posts&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code posts&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long periods without posts&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any of that sounds interesting, stick around.&lt;/p&gt;</content><category term="Meta"/><category term="meta"/><category term="introduction"/></entry></feed>