<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Clean on AdminJitsu</title>
    <link>https://adminjitsu.com/tags/clean/</link>
    <description>Recent content in Clean on AdminJitsu</description>
    <generator>Hugo -- 0.150.0</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 02 Oct 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://adminjitsu.com/tags/clean/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>SchemaSpy with LocalDB (SQL Server)</title>
      <link>https://adminjitsu.com/posts/schemaspy-mssql/</link>
      <pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://adminjitsu.com/posts/schemaspy-mssql/</guid>
      <description>Unlike MySQL or PostgreSQL, SQL Server LocalDB only speaks named pipes. Here’s the exact stack and command that makes SchemaSpy 6.1.0 work reliably with LocalDB on Windows.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>SchemaSpy is my preferred tool for generating <strong>browsable HTML docs and ER diagrams</strong> from SQL databases. I’ve already shown how to configure it with SQLite, but SQL Server’s <strong>LocalDB</strong> edition introduces a completely different set of quirks.</p>
<p>LocalDB is a lightweight SQL Server runtime used by tools like Lansweeper and Visual Studio. Unlike full SQL Server or Express, which run as services and listen on TCP, <strong>LocalDB doesn’t accept TCP connections at all</strong>. It only exposes a <strong>named pipe endpoint</strong>, and it only works with <strong>Windows integrated authentication (NTLM/SSPI)</strong>.</p>
<p>That’s why most SchemaSpy guides skip LocalDB entirely — Microsoft’s official JDBC driver expects TCP, and Java has no built-in NTLM support. The workaround is to use the older <strong>jTDS driver</strong>, which still supports named pipes, plus its companion <code>ntlmauth.dll</code> for NTLM authentication.</p>
<p>After a lot of trial and error, I found a reliable combination: <strong>SchemaSpy 6.1.0 + jTDS 1.3.3 + ntlmauth.dll + Graphviz 2.38</strong>. It’s fiddly, but once you have the pieces in place, you can generate a complete HTML schema doc site for any LocalDB-backed application.</p>
<br>
<h2 id="the-pieces-you-need">The Pieces You Need</h2>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Why It Matters</th>
          <th>Version / Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>SchemaSpy (fat JAR)</strong></td>
          <td>The doc/diagram generator</td>
          <td><strong>6.1.0</strong> (newer versions break jTDS)</td>
      </tr>
      <tr>
          <td><strong>jTDS JDBC Driver</strong></td>
          <td>Supports named pipes + NTLM/SSO</td>
          <td><strong>1.3.3</strong></td>
      </tr>
      <tr>
          <td><strong>Java Runtime</strong></td>
          <td>Runs SchemaSpy</td>
          <td>Java 8+ (64-bit recommended)</td>
      </tr>
      <tr>
          <td><strong>Graphviz</strong></td>
          <td>Renders static diagrams</td>
          <td><strong>2.38</strong> (stable with SchemaSpy 6.x)</td>
      </tr>
      <tr>
          <td><strong><code>ntlmauth.dll</code></strong></td>
          <td>Bridges NTLM/SSPI into Java for SSO</td>
          <td>Comes with jTDS zip (x86/x64/ia64)</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><span class="tag orange">Heads-up</span> Microsoft’s official JDBC driver <strong>cannot connect to LocalDB</strong> — it only works over TCP. That’s why jTDS is required.</p></blockquote>
<br>
<h2 id="suggested-setup-windows">Suggested Setup (Windows)</h2>
<p>Keep everything SchemaSpy needs in one folder so paths are consistent:</p>
<pre tabindex="0"><code>C:\bin\SchemaSpy\
  schemaspy-6.1.0.jar     # SchemaSpy itself
  jtds-1.3.3.jar          # JDBC driver
  ntlmauth.dll            # From jTDS zip, for Windows SSO
</code></pre><p>Install Graphviz to its default location:</p>
<pre tabindex="0"><code>C:\Program Files (x86)\Graphviz2.38\
</code></pre><hr>
<p><span class="tag red">Important</span> <strong>About <code>ntlmauth.dll</code></strong><br>
The jTDS 1.3.3 zip includes three versions:</p>
<ul>
<li><code>x86\ntlmauth.dll</code> → use if you run 32-bit Java</li>
<li><code>x64\ntlmauth.dll</code> → use if you run 64-bit Java (most common today)</li>
<li><code>ia64\ntlmauth.dll</code> → legacy Itanium, almost never needed</li>
</ul>
<p>Copy the correct DLL into your <code>C:\bin\SchemaSpy\</code> folder. When running SchemaSpy, add:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">-Djava</span><span class="p">.</span><span class="py">library</span><span class="p">.</span><span class="n">path</span><span class="p">=</span><span class="s2">&#34;C:\bin\SchemaSpy&#34;</span>
</span></span></code></pre></div><p>If the bitness doesn’t match, you’ll see <code>UnsatisfiedLinkError</code> or “Login failed” errors.</p>
<br>
<h2 id="step-1-find-the-localdb-instance-pipe">Step 1: Find the LocalDB Instance Pipe</h2>
<p>LocalDB doesn’t have a fixed hostname like <code>localhost,1433</code>. Instead, every time it starts, Windows assigns it a <strong>unique named pipe</strong>. That pipe is what jTDS connects to.</p>
<p>To check the pipe, open <strong>PowerShell or Command Prompt on Windows</strong> (on the same machine that’s running LocalDB) and run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">SqlLocalDB</span> <span class="nb">start </span><span class="n">LSInstance</span>
</span></span><span class="line"><span class="cl"><span class="n">SqlLocalDB</span> <span class="n">info</span>  <span class="n">LSInstance</span>
</span></span></code></pre></div><p>This will print details about the LocalDB instance, including the <strong>Instance pipe name</strong>. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">np:\\.\pipe\LOCALDB#SH964109\tsql\query
</span></span></code></pre></div><p>The important part is the <strong>instance token</strong> (<code>LOCALDB#SH964109</code>). You’ll pass that value into SchemaSpy with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">-instance &#34;LOCALDB#SH964109&#34;
</span></span></code></pre></div><p><span class="tag orange">Heads-up</span>: The token changes <strong>every time LocalDB restarts</strong>. If SchemaSpy suddenly stops connecting, rerun <code>SqlLocalDB info</code> to grab the updated string.</p>
<br>
<h2 id="step-2-authentication-options">Step 2: Authentication Options</h2>
<p>SQL Server supports two authentication modes, but only one works with LocalDB:</p>
<ul>
<li>
<p><strong>Windows Integrated (SSO)</strong><br>
LocalDB uses Windows credentials instead of SQL usernames. The account that runs SchemaSpy must be a Windows user who has been granted access to the database.</p>
<p>By default, this is the same account that installed or created the LocalDB instance (for example, the Windows user who installed Lansweeper). If you run SchemaSpy as a different user — even another local admin — you may get <code>Login failed for user</code> until that account is explicitly added as a login in SQL Server Management Studio.</p>
<p>To make NTLM authentication work in Java, you also need <code>ntlmauth.dll</code> from the jTDS package. Without it, SchemaSpy will fail to authenticate even if the Windows account has access.</p>
</li>
<li>
<p><strong>SQL Login (username/password)</strong><br>
This works on full SQL Server or Express, where TCP/IP is enabled and you can create dedicated logins. LocalDB does <strong>not</strong> allow this mode out of the box, so you can’t rely on <code>-u/-p</code> against a stock LocalDB instance.</p>
</li>
</ul>
<br>
<h2 id="step-3-the-working-command">Step 3: The Working Command</h2>
<p>With SchemaSpy, jTDS, and Graphviz in place, you’re ready to run the actual command. This is where everything comes together.</p>
<blockquote>
<p><span class="tag red">Important</span> SchemaSpy appends <code>\bin\dot</code> to whatever you provide via <code>-gv</code>. Always pass the <strong>Graphviz install root folder</strong>, not the path to <code>dot.exe</code>.</p></blockquote>
<br>
<h3 id="option-1-windows-integrated-authentication-sso">Option 1: Windows Integrated Authentication (SSO)</h3>
<p>This is the <strong>normal way</strong> to connect to LocalDB. SchemaSpy will log in as the Windows account you’re running under (make sure that account has database access). You also need <code>ntlmauth.dll</code> and <code>-Djava.library.path</code>.</p>
<p><strong>If you’re running from <code>cmd.exe</code>:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cmd" data-lang="cmd"><span class="line"><span class="cl">java -Djava.library.path=<span class="s2">&#34;C:\bin\SchemaSpy&#34;</span> -jar <span class="s2">&#34;C:\bin\SchemaSpy\schemaspy-6.1.0.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -t mssql-jtds-instance <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -dp <span class="s2">&#34;C:\bin\SchemaSpy\jtds-1.3.3.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -host . <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -instance <span class="s2">&#34;LOCALDB#SH964109&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -db lansweeperdb <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -s dbo <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -sso <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -connprops <span class="s2">&#34;namedPipe\=true&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -o <span class="s2">&#34;C:\bin\SchemaSpy\output&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -gv <span class="s2">&#34;C:\Program Files (x86)\Graphviz2.38&#34;</span>
</span></span></code></pre></div><p><strong>If you’re running from PowerShell:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">java</span> <span class="n">-Djava</span><span class="p">.</span><span class="py">library</span><span class="p">.</span><span class="n">path</span><span class="p">=</span><span class="s2">&#34;C:\bin\SchemaSpy&#34;</span> <span class="n">-jar</span> <span class="s2">&#34;C:\bin\SchemaSpy\schemaspy-6.1.0.jar&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-t</span> <span class="nb">mssql-jtds</span><span class="n">-instance</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-dp</span> <span class="s2">&#34;C:\bin\SchemaSpy\jtds-1.3.3.jar&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-host</span> <span class="p">.</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-instance</span> <span class="s2">&#34;LOCALDB#SH964109&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-db</span> <span class="n">lansweeperdb</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-s</span> <span class="n">dbo</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-sso</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-connprops</span> <span class="s2">&#34;namedPipe\=true&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-o</span> <span class="s2">&#34;C:\bin\SchemaSpy\output&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">-gv</span> <span class="s2">&#34;C:\Program Files (x86)\Graphviz2.38&#34;</span>
</span></span></code></pre></div><p><span class="tag orange">Heads-up</span> Use <code>^</code> in Command Prompt and <code>`</code> (backtick) in PowerShell. Mixing them up will cause errors like <code>'-t' is not recognized as an internal or external command</code>.</p>
<br>
<h3 id="option-2-sql-login-if-one-exists-in-localdb">Option 2: SQL Login (if one exists in LocalDB)</h3>
<p>Some applications (like Lansweeper) create a SQL login inside LocalDB (<code>lansweeperuser</code>). If such a login exists, you can use it with <code>-u</code>/<code>-p</code> instead of SSO. The continuation rules are the same as above.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cmd" data-lang="cmd"><span class="line"><span class="cl">java -jar <span class="s2">&#34;C:\bin\SchemaSpy\schemaspy-6.1.0.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -t mssql-jtds-instance <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -dp <span class="s2">&#34;C:\bin\SchemaSpy\jtds-1.3.3.jar&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -host . <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -instance <span class="s2">&#34;LOCALDB#SH964109&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -db lansweeperdb <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -s dbo <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -u lansweeperuser -p <span class="s2">&#34;SuperSecret&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -connprops <span class="s2">&#34;namedPipe\=true&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -o <span class="s2">&#34;C:\bin\SchemaSpy\output&#34;</span> <span class="se">^
</span></span></span><span class="line"><span class="cl"><span class="se"> </span> -gv <span class="s2">&#34;C:\Program Files (x86)\Graphviz2.38&#34;</span>
</span></span></code></pre></div><p><span class="tag blue">Note</span> If no SQL login exists in your LocalDB instance, this will fail with <em>Login failed for user</em>. In that case, you must use <strong>SSO</strong>.</p>
<br>
<h3 id="flag-breakdown">Flag Breakdown</h3>
<ul>
<li><code>-t mssql-jtds-instance</code> → use the jTDS SQL Server driver profile.</li>
<li><code>-dp</code> → path to the jTDS JAR file.</li>
<li><code>-host .</code> + <code>-instance &quot;LOCALDB#...&quot;</code> → connect via the LocalDB named pipe.</li>
<li><code>-db lansweeperdb</code> → database name to document.</li>
<li><code>-s dbo</code> → schema to include (typically <code>dbo</code>).</li>
<li><code>-sso</code> → use Windows integrated authentication.</li>
<li><code>-u/-p</code> → alternative, if a SQL login exists.</li>
<li><code>-connprops &quot;namedPipe=true&quot;</code> → <strong>critical</strong>: forces jTDS to use named pipes instead of TCP.</li>
<li><code>-gv &quot;&lt;GraphvizRoot&gt;&quot;</code> → Graphviz install root (SchemaSpy appends <code>\bin\dot</code>).</li>
<li><code>-o</code> → output directory for the generated site.</li>
</ul>
<br>
<h3 id="what-normal-output-looks-like">What Normal Output Looks Like</h3>
<p>SchemaSpy is <em>chatty</em>. A successful run doesn’t just say “Connected” — you’ll see:</p>
<ul>
<li>ASCII-art SchemaSpy banner and license text.</li>
<li>Warnings about restricted methods (expected with jTDS + modern Java).</li>
<li>A line like:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">INFO  - Connected to Microsoft SQL Server - 16.00.xxxx
</span></span></code></pre></div></li>
<li>Long “Gathering schema details…” progress lines with dots.</li>
<li>Occasional warnings about <code>sysproperties</code> (safe to ignore — SQL Server 2016+ removed it).</li>
<li>Graphviz messages about “graph is too large for cairo-renderer” with scaling (also safe).</li>
<li>A final summary like:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Wrote relationship details of 1274 tables/views to directory &#39;C:\bin\SchemaSpy\output&#39;
</span></span><span class="line"><span class="cl">View the results by opening C:\bin\SchemaSpy\output\index.html
</span></span></code></pre></div></li>
</ul>
<p>The key thing to look for is that <strong>output files get written</strong> and you see a final “View the results…” line. Everything else (warnings, scaling errors, missing comments) is normal noise.</p>
<p>The output directory will contain a full HTML site with:</p>
<ul>
<li><code>index.html</code> → overview page</li>
<li><code>tables/</code> → per-table pages</li>
<li><code>columns/</code> → per-column detail pages</li>
<li><code>relationships/</code> → ER diagrams (static images + clickable maps)</li>
</ul>
<p>Open <code>index.html</code> in your browser to start exploring.</p>
<br>
<h2 id="why-this-works">Why This Works</h2>
<ul>
<li><strong>SchemaSpy 6.1.0</strong> preserves working integration with <code>mssql-jtds-instance</code>.</li>
<li><strong>jTDS 1.3.3</strong> supports <strong>named pipes</strong> and <strong>Windows SSO</strong> (critical for LocalDB).</li>
<li><code>-connprops &quot;namedPipe=true&quot;</code> routes through the LocalDB pipe.</li>
<li><strong>Graphviz 2.38</strong> is the last hassle-free version for SchemaSpy 6.x.</li>
<li><strong><code>ntlmauth.dll</code></strong> provides NTLM/SSPI so SSO works from Java.</li>
</ul>
<br>
<h2 id="troubleshooting-matrix">Troubleshooting Matrix</h2>
<p>Even with the right jars and flags, LocalDB is fiddly. Here’s a quick reference for the most common errors and how to resolve them:</p>
<table>
  <thead>
      <tr>
          <th>Symptom / Error</th>
          <th>Likely Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Login failed for user</code> (SSO)</td>
          <td><code>ntlmauth.dll</code> missing or wrong bitness</td>
          <td>Put <code>ntlmauth.dll</code> alongside the JARs; match your Java runtime (x64 vs x86); include <code>-Djava.library.path=&quot;C:\bin\SchemaSpy&quot;</code></td>
      </tr>
      <tr>
          <td><code>The TCP/IP connection to the host…</code></td>
          <td>Using Microsoft’s JDBC driver, or forgot <code>namedPipe=true</code></td>
          <td>Use <strong>jTDS 1.3.3</strong>; add <code>-connprops &quot;namedPipe\=true&quot;</code></td>
      </tr>
      <tr>
          <td><code>Cannot open database &quot;&lt;dbname&gt;&quot; requested by the login</code></td>
          <td>Wrong DB name or missing permissions</td>
          <td>Verify the database name (e.g. <code>lansweeperdb</code> for Lansweeper, or your own app’s DB); check that your Windows user or SQL login has access</td>
      </tr>
      <tr>
          <td><code>No suitable driver</code></td>
          <td>Driver not found</td>
          <td>Ensure <code>-dp &quot;C:\bin\SchemaSpy\jtds-1.3.3.jar&quot;</code> is correct</td>
      </tr>
      <tr>
          <td>Diagrams not generated</td>
          <td>Wrong <code>-gv</code> path or Graphviz missing</td>
          <td>Point <code>-gv</code> to the <strong>Graphviz root folder</strong> (e.g., <code>...\Graphviz2.38</code>), not <code>dot.exe</code>; reinstall 2.38 if necessary</td>
      </tr>
      <tr>
          <td>Works once, fails after reboot</td>
          <td>LocalDB pipe name changed</td>
          <td>Re-run <code>SqlLocalDB info &lt;InstanceName&gt;</code> to get the new pipe, update <code>-instance &quot;LOCALDB#...&quot;</code></td>
      </tr>
      <tr>
          <td><code>The network path was not found</code></td>
          <td>Instance not started or pipe unavailable</td>
          <td>Run <code>SqlLocalDB start &lt;InstanceName&gt;</code>; confirm the pipe string; if using SSO, run SchemaSpy as the same Windows user that owns the DB</td>
      </tr>
      <tr>
          <td>Non-ASCII path weirdness</td>
          <td>Path quoting/escaping on Windows</td>
          <td>Wrap all paths in quotes; avoid special characters in folder names if possible</td>
      </tr>
  </tbody>
</table>
<br>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<ul>
<li>
<p><strong>SchemaSpy</strong><br>
📥 <a href="https://github.com/schemaspy/schemaspy/releases/download/v6.1.0/schemaspy-6.1.0.jar">SchemaSpy v6.1.0 JAR (GitHub)</a><br>
<a href="https://schemaspy.readthedocs.io/">SchemaSpy Documentation</a></p>
</li>
<li>
<p><strong>jTDS JDBC Driver (1.3.3)</strong><br>
📥 <a href="https://sourceforge.net/projects/jtds/files/jtds/1.3.3/">Download jTDS 1.3.3 (SourceForge)</a><br>
<a href="https://web.archive.org/web/20160331133731/http://jtds.sourceforge.net/">Archived jTDS project site</a></p>
</li>
<li>
<p><strong>Graphviz (2.38 for Windows)</strong><br>
📥 <a href="https://graphviz.gitlab.io/_pages/Download/windows/graphviz-2.38.msi">Graphviz 2.38 Windows installer (archive)</a><br>
<a href="https://graphviz.org/">Graphviz site &amp; docs</a></p>
</li>
<li>
<p><strong>SQL Server LocalDB</strong><br>
<a href="https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-express-localdb">Microsoft Docs – SQL Server Express LocalDB</a></p>
</li>
<li>
<p><strong>SQL Server Management Studio (SSMS)</strong><br>
📥 <a href="https://aka.ms/ssmsfullsetup">Download SSMS (Microsoft)</a><br>
Handy for browsing your LocalDB, verifying database names (e.g. <code>lansweeperdb</code>), and checking which logins exist.</p>
</li>
</ul>
<p><em>(For long-term use, consider mirroring the required JARs and DLLs internally so your team isn’t chasing archive sites years later.)</em></p>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>LocalDB’s <strong>named-pipe-only</strong> quirk trips up the usual JDBC approach. The dependable combo is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SchemaSpy 6.1.0  +  jTDS 1.3.3  +  -connprops &#34;namedPipe=true&#34;  +  Graphviz 2.38
</span></span></code></pre></div><p>With that in place — and the LocalDB <strong>instance pipe</strong> refreshed as needed — you’ll get clean, searchable HTML docs and ERDs for any LocalDB-backed app (e.g. Lansweeper, Visual Studio projects).</p>
<p>If you run into an edge case I didn’t cover, send it my way <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a> and I’ll expand the troubleshooting matrix.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Tcpdump Survival Guide</title>
      <link>https://adminjitsu.com/posts/tcpdump-survival-guide/</link>
      <pubDate>Wed, 24 Sep 2025 05:50:39 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/tcpdump-survival-guide/</guid>
      <description>This guide covers the essentials of packet capture: where to listen, how to plan, saving full packets to PCAP, filtering for relevance, and capturing iOS and Android traffic, troubleshooting, automation, and handing off to Wireshark for analysis.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>Tcpdump is the <strong>Swiss Army knife of packet capture</strong>. It’s lean, scriptable, and ships with almost every Unix-like system. That makes it ideal for automation, troubleshooting, or handing to a customer when you need a reproducible capture without walking them through a GUI.</p>
<p>Most people analyze captures in something like <strong>Wireshark</strong>, a solid open-source packet analyzer. But tcpdump is often the better tool for actually collecting the data in the first place since it can be scripted, shared as a one-liner, and run with minimal instructions.</p>
<p>Packet captures are rarely trivial though: they can result in big files, require various permissions, approvals, and scheduled maintenance windows. They&rsquo;re not something you want to repeat needlessly. The goal is to get it right <strong>the first time</strong> .</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="saknife.jpg" 
       alt="Swiss Army Knife Climber model" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
<figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
<p>Photo: Ave Maria / Jonas, via <a href="https://commons.wikimedia.org/wiki/File:Victorinox_Swiss_Army_Knife_-_Climber_(15554551505).jpg" target="_blank">Wikimedia Commons</a>,
<a href="https://creativecommons.org/licenses/by/2.0/" target="_blank">CC BY 2.0</a></p>
</figcaption>
</figure>
<br>
<h2 id="where-to-capture">Where to Capture?</h2>
<p>Before worrying about command flags, it helps to think about <strong>point of view</strong> you need to target. Packet captures are always relative. What you see depends on where you’re listening from.</p>
<p>If you’re debugging a client/server exchange, you could capture on the client to confirm whether requests are leaving, or on the server to see if they arrive. Sometimes you need both perspectives: one to prove the client sent the packet, the other to prove the server received it (or didn’t).</p>
<p>The network itself matters too. On a flat LAN or broadcast domain, a capture may show you traffic between many hosts. But on a switched or routed network, you’ll only see packets destined to or from the machine you’re capturing on. That’s why capturing on the endpoint itself is often the simplest, most reliable approach.</p>
<p>For complex problems, it’s common to gather multiple captures — one at the client, one at the server, perhaps one taken at a switch or router. When you line them up by timestamp, you can follow the packet’s journey across the path and pinpoint where things break down.</p>
<p>Once you know where you need to capture from, you can follow a simple workflow:</p>
<ul>
<li>Start capture</li>
<li>Start Repro (it&rsquo;s really helpful to take notes and record the time you start, when you notice a behavior or error etc)</li>
<li>Stop capture</li>
<li>Analyze in Wireshark or a similar tool</li>
</ul>
<br>
<h2 id="choosing-the-right-interface">Choosing the Right Interface</h2>
<p>Once you know where you want to capture from you&rsquo;ll need to choose the correct interface. This varies by system but typically you want Ethernet or Wi-fi. On modern systems, there can be a lot of interfaces for things like VPN tunnels, virtual intefaces and so on.</p>
<p>List available interfaces with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tcpdump -D
</span></span></code></pre></div><p>On my system i get this potentially confusing list:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">1.loopback0 <span class="o">[</span>Up, Running, Connected<span class="o">]</span>
</span></span><span class="line"><span class="cl">2.eth2 <span class="o">[</span>Up, Running, Connected<span class="o">]</span>
</span></span><span class="line"><span class="cl">3.eth0 <span class="o">[</span>Up, Running, Connected<span class="o">]</span>
</span></span><span class="line"><span class="cl">4.any <span class="o">(</span>Pseudo-device that captures on all interfaces<span class="o">)</span> <span class="o">[</span>Up, Running<span class="o">]</span>
</span></span><span class="line"><span class="cl">5.lo <span class="o">[</span>Up, Running, Loopback<span class="o">]</span>
</span></span><span class="line"><span class="cl">6.docker0 <span class="o">[</span>Up, Disconnected<span class="o">]</span>
</span></span><span class="line"><span class="cl">7.bluetooth-monitor <span class="o">(</span>Bluetooth Linux Monitor<span class="o">)</span> <span class="o">[</span>Wireless<span class="o">]</span>
</span></span><span class="line"><span class="cl">8.nflog <span class="o">(</span>Linux netfilter log <span class="o">(</span>NFLOG<span class="o">)</span> interface<span class="o">)</span> <span class="o">[</span>none<span class="o">]</span>
</span></span><span class="line"><span class="cl">9.nfqueue <span class="o">(</span>Linux netfilter queue <span class="o">(</span>NFQUEUE<span class="o">)</span> interface<span class="o">)</span> <span class="o">[</span>none<span class="o">]</span>
</span></span><span class="line"><span class="cl">10.dbus-system <span class="o">(</span>D-Bus system bus<span class="o">)</span> <span class="o">[</span>none<span class="o">]</span>
</span></span><span class="line"><span class="cl">11.dbus-session <span class="o">(</span>D-Bus session bus<span class="o">)</span> <span class="o">[</span>none<span class="o">]</span>
</span></span><span class="line"><span class="cl">12.eth1 <span class="o">[</span>none, Disconnected<span class="o">]</span>
</span></span><span class="line"><span class="cl">13.eth3 <span class="o">[</span>none, Disconnected<span class="o">]</span>
</span></span><span class="line"><span class="cl">14.eth4 <span class="o">[</span>none, Disconnected<span class="o">]</span>
</span></span></code></pre></div><p>Most of these aren&rsquo;t very interesting, like loopback, D-Bus, netfilter hooks etc. You typically want your Ethernet or Wi-Fi interface (the eth0, eth2.. interfaces in my list. You might see <code>ens33</code> or <code>wlan0</code> or <code>en0</code> on yours).</p>
<p>You can get a more helpful list with a simple filter on the interface flags:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tcpdump -D <span class="p">|</span> grep Connected
</span></span></code></pre></div><p>That returns a much more reasonable list although loopback is always a valid connected interface so it remains:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ tcpdump -D <span class="p">|</span> grep Connected
</span></span><span class="line"><span class="cl">1.loopback0 <span class="o">[</span>Up, Running, Connected<span class="o">]</span>
</span></span><span class="line"><span class="cl">2.eth2 <span class="o">[</span>Up, Running, Connected<span class="o">]</span>
</span></span><span class="line"><span class="cl">3.eth0 <span class="o">[</span>Up, Running, Connected<span class="o">]</span>
</span></span></code></pre></div><p>You usually won&rsquo;t be interested in loopback so that leaves two ethernet connections to choose from. If you&rsquo;re not sure, you can usually just check to see which one has the ip address you want to capture on:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ifconfig eth0
</span></span></code></pre></div><p>On a lot of newer linuxes, the command is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip addr show eth0
</span></span></code></pre></div><p>If it&rsquo;s still not clear, you can check to see which of the interfaces is &ldquo;busy&rdquo; and take a peek at the traffic. To check an interface you can grab a quick capture with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -c <span class="m">20</span>
</span></span></code></pre></div><p>On my system, both eth0 and eth2 are active but within seconds of watching tcpdump on eth2 i could recognize familiar ips and dns names from my lan.</p>
<br>
<h2 id="gathering-a-solid-capture">Gathering a Solid Capture</h2>
<p>Now that you’ve thought about where to capture and how to focus your test, the next step is setting up tcpdump to record everything you need without losing detail.</p>
<h3 id="save-to-a-file">Save to a File</h3>
<p>For later analysis in Wireshark or another tool, always write to a <code>.pcap</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -w capture.pcap
</span></span></code></pre></div><ul>
<li><code>-s 0</code> → capture the <strong>entire packet</strong>, not just the default snap length.</li>
<li><code>-w</code>   → write raw packets to a file (not human-readable).</li>
<li>Use <code>Ctrl+C</code> to stop when you’ve got enough.</li>
</ul>
<h3 id="quick-checks">Quick Checks</h3>
<p>Sometimes you only need a small slice:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -c <span class="m">500</span> -w sample.pcap
</span></span></code></pre></div><ul>
<li><code>-c</code> stops after N packets.</li>
<li>Great for reproducible test runs.</li>
</ul>
<h3 id="reading-it-back">Reading It Back</h3>
<p>To verify or skim without Wireshark:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tcpdump -r capture.pcap
</span></span></code></pre></div><h3 id="pro-tips">Pro Tips</h3>
<ul>
<li>Always run with <code>sudo</code> (or root).</li>
<li>Watch disk space — captures grow quickly.</li>
<li>Time matters: make sure your system clock is accurate for useful logs.</li>
</ul>
<br>
<br>
<h2 id="capturing-mobile-traffic">Capturing Mobile Traffic</h2>
<p>Both iOS and Android expose interfaces that tcpdump can hook into from a computer. In both cases you’ll need the device <strong>plugged in with a USB cable</strong>.</p>
<hr>
<h3 id="ios-macos-host-required">iOS (macOS host required)</h3>
<p>On macOS, Apple provides a <strong>Remote Virtual Interface (RVI)</strong> that exposes iPhone or iPad network traffic over USB. The device must be <strong>unlocked</strong> and you’ll need to tap <em>Trust this computer</em> the first time you connect. Once enabled, the phone appears as a virtual interface (<code>rvi0</code>) that you can capture from with tcpdump.</p>
<p>The only catch: you need the device’s <strong>UDID</strong> (Unique Device Identifier). You can get it a few ways:</p>
<ul>
<li>
<p><strong>System Profiler (built-in):</strong><br>
Plug in your device, then run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">system_profiler SPUSBDataType <span class="p">|</span> grep -w <span class="s2">&#34;Serial Number&#34;</span>
</span></span></code></pre></div><p>This prints the serial numbers of connected USB devices. For iPhones and iPads, that string is the UDID.</p>
</li>
<li>
<p><strong>libimobiledevice (third-party tools):</strong><br>
If you have <a href="https://libimobiledevice.org/">libimobiledevice</a> installed, you can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">idevice_id -l
</span></span></code></pre></div><p>which lists all connected iOS devices by UDID.</p>
</li>
</ul>
<p><span class="tag green">Pro-Tip</span> If you’re giving instructions to someone else, you can make it a one-liner:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -s <span class="k">$(</span>system_profiler SPUSBDataType <span class="p">|</span> awk <span class="s1">&#39;/Serial Number/{print $3; exit}&#39;</span><span class="k">)</span>
</span></span></code></pre></div><p>That command automatically grabs the first iOS device’s UDID and passes it to <code>rvictl</code>.</p>
<p>Once the RVI is active, check with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -l
</span></span></code></pre></div><p>When finished, tear it down with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rvictl -x &lt;device-udid&gt;
</span></span></code></pre></div><p>At that point, <code>rvi0</code> behaves like any other interface. You can capture traffic from it just like you would on Ethernet or Wi-Fi:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i rvi0 -s <span class="m">0</span> -w iphone.pcap
</span></span></code></pre></div><p>This gives you a complete packet trace of the phone’s traffic.</p>
<hr>
<h3 id="android-usb-required">Android (USB required)</h3>
<p>Android doesn’t expose an RVI, but you can capture traffic via the <strong>Android Debug Bridge (adb)</strong>. Your phone must be <strong>plugged in over USB with USB debugging enabled</strong>. On many devices this works out of the box, but some builds require a tcpdump binary installed on the phone and <strong>root privileges</strong> to run it.</p>
<ul>
<li><strong>macOS:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install android-platform-tools
</span></span></code></pre></div></li>
<li><strong>Debian/Ubuntu:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install android-tools-adb
</span></span></code></pre></div></li>
<li><strong>Fedora:</strong>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dnf install android-tools
</span></span></code></pre></div></li>
<li><strong>Windows:</strong><br>
Download <a href="https://developer.android.com/tools/releases/platform-tools">Android Platform Tools</a> from Google.</li>
</ul>
<p>With adb installed and your phone connected by USB (with USB debugging enabled), verify the device is detected:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adb devices
</span></span></code></pre></div><p>You may need to accept a prompt on the phone.</p>
<p>From there, you have two approaches:</p>
<ul>
<li>
<p><strong>Capture directly on the device:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adb shell tcpdump -i any -s <span class="m">0</span> -w /sdcard/capture.pcap
</span></span><span class="line"><span class="cl">adb pull /sdcard/capture.pcap
</span></span></code></pre></div><p>This writes a <code>.pcap</code> file to the device and then pulls it back to your computer.</p>
</li>
<li>
<p><strong>Stream packets live to your desktop:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adb exec-out tcpdump -i any -s <span class="m">0</span> -w - &gt; android.pcap
</span></span></code></pre></div><p>This writes the capture directly to your computer for later analysis in Wireshark (or the moral equivalent).</p>
</li>
</ul>
<br>
<h2 id="filtering-for-relevance">Filtering for Relevance</h2>
<p>Captures get big quickly so it&rsquo;s important to filter out as much noise as possible. Tcpdump uses the <strong>Berkeley Packet Filter (BPF)</strong> syntax, which is fairly simple once you know a few basics.</p>
<p>In practice, you’ll usually want to capture <strong>full packets</strong> and write them to a file for later analysis in Wireshark. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tcpdump -i eth0 -s <span class="m">0</span> -w capture.pcap <span class="s1">&#39;tcp port 443 and host example.com&#39;</span>
</span></span></code></pre></div><ul>
<li><code>-s 0</code> → capture the <strong>entire packet</strong></li>
<li><code>-w capture.pcap</code> → write to a file instead of printing to the screen</li>
<li>Filter expression in quotes → what traffic to capture</li>
</ul>
<p>For the examples below, we’ll omit the <code>-s 0 -w ...</code> parts to keep things short, but remember to include them when you’re running a real capture.</p>
<h3 id="common-filters">Common Filters</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Host-based</span>
</span></span><span class="line"><span class="cl">tcpdump host 192.168.1.50
</span></span><span class="line"><span class="cl">tcpdump src host 10.0.0.5
</span></span><span class="line"><span class="cl">tcpdump dst host example.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Port-based</span>
</span></span><span class="line"><span class="cl">tcpdump port <span class="m">80</span>
</span></span><span class="line"><span class="cl">tcpdump tcp port <span class="m">443</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Protocol</span>
</span></span><span class="line"><span class="cl">tcpdump icmp
</span></span><span class="line"><span class="cl">tcpdump arp
</span></span><span class="line"><span class="cl">tcpdump udp
</span></span></code></pre></div><h3 id="practical-filtering-recipes">Practical Filtering Recipes</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># DNS traffic only</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 port <span class="m">53</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># HTTP traffic to a specific host</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 tcp port <span class="m">80</span> and host example.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># HTTPS traffic to a specific host</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 tcp port <span class="m">443</span> and host 203.0.113.42
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># All web traffic (HTTP + HTTPS) from one client</span>
</span></span><span class="line"><span class="cl">tcpdump -i eth0 host 192.168.1.25 and <span class="se">\(</span> port <span class="m">80</span> or port <span class="m">443</span> <span class="se">\)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Capture traffic in a subnet</span>
</span></span><span class="line"><span class="cl">tcpdump net 192.168.1.0/24
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Capture all but one noisy host</span>
</span></span><span class="line"><span class="cl">tcpdump not host 192.168.1.10
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># SSH traffic to or from a specific server</span>
</span></span><span class="line"><span class="cl">tcpdump tcp port <span class="m">22</span> and host bastion.example.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ICMP (pings) except from a certain host</span>
</span></span><span class="line"><span class="cl">tcpdump icmp and not src host 10.0.0.5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Multiple conditions: HTTPS to a subnet, but ignore one host</span>
</span></span><span class="line"><span class="cl">tcpdump tcp port <span class="m">443</span> and net 10.0.0.0/24 and not host 10.0.0.50
</span></span></code></pre></div><h3 id="tips">Tips</h3>
<ul>
<li>Use parentheses <code>()</code> when combining multiple expressions (escape them in shells with <code>\(</code> <code>\)</code>).</li>
<li>Add <code>-n</code> to prevent tcpdump from resolving hostnames (faster, less noise).</li>
<li>Increase detail with <code>-v</code>, <code>-vv</code>, or <code>-vvv</code> when reviewing packets live.</li>
<li>Filters in tcpdump (BPF) are not the same as <strong>Wireshark display filters</strong> — capture filters decide what packets are saved, while display filters decide what you see later.</li>
</ul>
<br>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<p><strong>Essential Documentation:</strong></p>
<ul>
<li><a href="https://www.tcpdump.org/manpages/tcpdump.1.html">Tcpdump man page</a></li>
<li><a href="https://www.tcpdump.org/">Tcpdump &amp; Libpcap homepage</a></li>
<li><a href="https://www.wireshark.org/docs/dfref/">Wireshark Display Filters</a></li>
</ul>
<p><strong>Further Reading:</strong></p>
<ul>
<li><a href="https://wizardzines.com/zines/tcpdump/">Tcpdump Zine by Julia Evans</a> — a fun, visual guide that complements this article.</li>
<li><a href="https://packetlife.net/library/cheat-sheets/">Packet Life Cheat Sheets</a> — quick reference sheets for tcpdump, Wireshark, and more.</li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>pcaps or it didn&rsquo;t happen!</p>
<p>If you found this post useful or if you have questions, I’d love to hear from you: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Unix Users Handbook</title>
      <link>https://adminjitsu.com/posts/unix-users-handbook/</link>
      <pubDate>Sun, 21 Sep 2025 14:12:00 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/unix-users-handbook/</guid>
      <description>Unix has always been multiuser. This guide explains why, shows you how identities are stored and enforced, and gives you a cross-platform, task-based cheatsheet of every user-related command you need.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>Unix isn’t a lonely single-player game</p>
<p>Even when adventuring on your personal laptop, you’re <strong>never alone</strong>. A whole party of user accounts travels with you: <code>root</code> with absolute power, <code>daemon</code> running background jobs, <code>nobody</code> handling scraps, and dozens of service identities (<code>mysql</code>, <code>www-data</code>, <code>postfix</code>). They own files, run processes, and enforce boundaries between users.</p>
<p>Unix was built around the idea of a multiuser, shared environment. In the 1970s, a single minicomputer would act as a hub for dozens of dumb terminals (green-screen VT100s, Teletypes, or glass TTYs) all wired in over RS-232 serial lines. Later, SLIP and PPP links carried those sessions across early networks. Everyone logged in concurrently, sharing the same CPU, disks, and memory. To survive in that environment, the OS enforced strict user separation: each login session mapped to a unique UID, file ownership was enforced at the kernel level, and processes were isolated by identity.</p>
<p>This guide explains:</p>
<ul>
<li><strong>History and concepts</strong> of Unix users</li>
<li><strong>Where identities live</strong> (<code>/etc/passwd</code>, <code>/etc/shadow</code>, <code>/etc/group</code>, <code>/etc/sudoers</code>)</li>
<li>A <strong>cross-platform, task-based cheatsheet</strong> covering everything from inspecting to creating, modifying, and deleting users, plus group management and sudo.</li>
<li>Historical tidbits, gotchas, and modern tricks</li>
<li>All the pertinent documentation and links</li>
</ul>
<p><span class="tag orange">NOTE</span>
By the end you’ll have your own <strong>player&rsquo;s screen</strong> — every user-related command condensed into one place and neatly organized by task.</p>
<br>
<h2 id="so-what-is-a-user">So, What is a User?</h2>
<p>In Unix, a “user” is just an <strong>identity</strong> (a user principal) that the kernel uses to decide what’s allowed. Every process runs <em>as someone</em>, every file is <em>owned by someone</em>, and the system enforces permissions based on those identities.</p>
<p>At the machine level, a user boils down to three things:</p>
<ul>
<li><strong>UID (User ID):</strong> a unique integer (UID). This is what the kernel actually checks. UID <code>0</code> is hard-coded as <code>root</code>, the all-powerful superuser.</li>
<li><strong>GID (Group ID):</strong> groups are collections of users, identified by their own numeric IDs (GID). Group permissions are checked alongside individual user permissions.</li>
<li><strong>Username:</strong> a human-friendly alias for a UID. When you type <code>alice</code> at a login prompt, the system maps that to UID <code>1000</code> (or whatever number was assigned).</li>
</ul>
<p>The kernel doesn&rsquo;t care about names, only numbers. When you run <code>ls -l</code>, it looks up UIDs and GIDs in <code>/etc/passwd</code> and <code>/etc/group</code> just to display friendlier, human-readable names.</p>
<p><span class="tag orange">NOTE</span> If you delete a user but keep their files, ownership doesn’t vanish. The UID stays behind, and you’ll see files owned by <code>1001</code> or <code>2000</code>, etc. That’s like the <strong>ghost of a user</strong>. The identity is gone, but the number persists in the filesystem.</p>
<p>This split between human-friendly names and machine-level numbers is deliberate. It means accounts can be automated, identities can be isolated, and multiuser systems don’t collapse into chaos.</p>
<br>
<h2 id="history--tidbits">History &amp; Tidbits</h2>
<p>Unix grew up in a world where multiuser systems were the norm.</p>
<ul>
<li><strong>1970s multiuser terminals:</strong> A single PDP-11 or VAX might have dozens of dumb terminals (Teletypes, VT100s, Wyse displays) connected over serial lines. Each terminal was just a keyboard and screen; all the real computing happened on the host. Later, SLIP and PPP carried terminal sessions over early TCP/IP links, making multiuser logins possible from remote sites.</li>
<li><strong>Strict separation of users:</strong> Because many people were logged in simultaneously, the kernel had to enforce sharp boundaries. Each session mapped to a unique UID; processes couldn’t touch each other’s files; and permissions were checked on every system call.</li>
<li><strong>UID 0:</strong> Always <code>root</code>. The kernel literally has “if (uid == 0)” checks hard-coded in privileged operations. This convention has survived intact for 50+ years.</li>
<li><strong>System accounts:</strong> Services needed their own identities to run safely. Instead of everything being <code>root</code>, daemons like <code>mail</code>, <code>www-data</code>, and <code>mysql</code> got UIDs of their own. That way, a web server compromise didn’t instantly mean total system takeover.</li>
<li><strong>Different OS ranges:</strong>
<ul>
<li><strong>Linux:</strong> human users typically start at UID <strong>1000</strong> today (but <strong>500</strong> on older RHEL/CentOS). Below that are system accounts.</li>
<li><strong>macOS:</strong> human users start at <strong>501</strong>; everything below is reserved. Apple prefixes many system accounts with underscores (<code>_spotlight</code>, <code>_windowserver</code>).</li>
<li><strong>BSD:</strong> similar to Linux, but ranges vary; users typically start at UID <strong>1001</strong>; service accounts and reserved IDs are well documented in the BSD handbooks.</li>
</ul>
</li>
<li><strong>“Nobody” user:</strong> UID <code>65534</code> (or sometimes <code>-2</code>) is a special identity with the least privilege possible. It exists to run processes with <em>almost no rights at all</em>.</li>
</ul>
<br>
<h2 id="the-user-database">The User Database</h2>
<p>So where do these identities actually live? In classic Unix, they’re just flat text files:</p>
<ul>
<li><strong><code>/etc/passwd</code></strong> — the account roster. Username, UID, GID, home directory, shell, and a password placeholder. World-readable.</li>
<li><strong><code>/etc/shadow</code></strong> — password hashes and aging rules. Only root can read it. If this file leaks, the system is blown.</li>
<li><strong><code>/etc/group</code></strong> — group definitions and memberships. Controls shared access.</li>
<li><strong><code>/etc/sudoers</code></strong> — the privilege ledger. Defines who can become root (and how).</li>
</ul>
<figure style="float:left; margin:0 1rem 1rem 0; width:clamp(260px, 45%, 450px);">
  <img src="spellbook.jpg" 
       alt="a pixel art depiction of an open book with mystical symbols" 
       style="display:block; width:100%; height:auto;">
  <figcaption style="font-size:85%; color:#666; line-height:1.4; margin-top:0.4em;">
    A wild, AI spellbook appears before you. 
  </figcaption>
</figure>
<p>On modern enterprise systems, these files often act as a <strong>front-end to NSS</strong> (Name Service Switch), which may pull identities from LDAP, NIS, Kerberos, or Active Directory. That’s why tools like <code>getent</code> are preferred over <code>cat /etc/passwd</code> .They return the whole picture not just a local slice of it.</p>
<p>Think of these files as the <strong>bones</strong> of an identity. The commands you run (like <code>passwd</code>, <code>useradd</code>, <code>dscl</code>, <code>visudo</code>) are the muscles that safely move those bones around.</p>
<p>And above it all sits the kernel, acting as the <strong>nervous system</strong>.<br>
It doesn’t care whether an identity came from a flat file or a directory<br>
service; all it sees are UIDs and GIDs. That abstraction is the secret<br>
that lets Unix scale from a single laptop to a campus full of machines<br>
while keeping the rules of identity consistent.</p>
<div style="clear:both"></div>
<br>
<h2 id="permissions--ownership-recap">Permissions &amp; Ownership Recap</h2>
<p>Users only matter because the kernel enforces <strong>who owns what</strong> and <strong>who can do what</strong>. That enforcement happens at two levels: <strong>files</strong> and <strong>processes</strong>.</p>
<h3 id="file-permissions">File Permissions</h3>
<p>Every file has two owners:</p>
<ul>
<li>a <strong>user owner</strong> (UID)</li>
<li>a <strong>group owner</strong> (GID)</li>
</ul>
<p>And three sets of permissions: <strong>user (u)</strong>, <strong>group (g)</strong>, and <strong>other (o)</strong>.</p>
<p>Example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">-rwxr-sr-- <span class="m">1</span> alice devs <span class="m">532</span> Sep <span class="m">21</span> 13:01 script.sh
</span></span></code></pre></div><p>Breakdown:</p>
<ul>
<li><strong><code>-</code></strong> → type of file. (<code>-</code> = regular file, <code>d</code> = directory, <code>l</code> = symlink, <code>c</code>/<code>b</code> = device, <code>s</code> = socket, <code>p</code> = named pipe)</li>
<li><strong><code>rwx</code></strong> → permissions for the <strong>user/owner</strong> (<code>alice</code>): read, write, execute.</li>
<li><strong><code>r-s</code></strong> → permissions for the <strong>group</strong> (<code>devs</code>): read + execute, plus <code>s</code> meaning <strong>setgid</strong> is set.</li>
<li><strong><code>r--</code></strong> → permissions for <strong>others</strong>: read only.</li>
<li><strong><code>1</code></strong> → hard link count. Directories show how many subdirs + self + parent.</li>
<li><strong><code>alice</code></strong> → the owner (mapped from UID).</li>
<li><strong><code>devs</code></strong> → the group (mapped from GID).</li>
<li><strong><code>532</code></strong> → file size in bytes.</li>
<li><strong><code>Sep 21 13:01</code></strong> → last modification time.</li>
<li><strong><code>script.sh</code></strong> → filename.</li>
</ul>
<p>Other permission bits:</p>
<ul>
<li><strong>setuid (<code>s</code> on user perms):</strong> program runs with file owner’s UID. Example: <code>/usr/bin/passwd</code> runs as root.</li>
<li><strong>setgid (<code>s</code> on group perms):</strong> files created in this directory inherit the group; executables run with group’s GID.</li>
<li><strong>sticky bit (<code>t</code> on others perms):</strong> on directories, only file owners can delete their files. <code>/tmp</code> uses this.</li>
</ul>
<p>💡 <em>Gotcha:</em> Permissions are checked <strong>on every system call</strong> (<code>open()</code>, <code>execve()</code>, etc). There’s no “once at login” caching — enforcement is continuous.</p>
<hr>
<h3 id="process-ownership">Process Ownership</h3>
<p>Every process runs <strong>as a user</strong> and carries both a <strong>real UID</strong> (who started it) and an <strong>effective UID</strong> (who it’s acting as).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ps -u alice
</span></span><span class="line"><span class="cl">UID   PID  CMD
</span></span><span class="line"><span class="cl"><span class="m">1000</span>  <span class="m">2345</span> bash
</span></span><span class="line"><span class="cl"><span class="m">1000</span>  <span class="m">2371</span> vim
</span></span><span class="line"><span class="cl"><span class="m">0</span>     <span class="m">2402</span> sudo
</span></span><span class="line"><span class="cl"><span class="m">0</span>     <span class="m">2403</span> systemctl
</span></span></code></pre></div><p>Here:</p>
<ul>
<li>Alice owns her <code>bash</code> and <code>vim</code> processes.</li>
<li>When she runs <code>sudo systemctl</code>, the new process has UID 0 (root).</li>
</ul>
<p>Key fields:</p>
<ul>
<li><strong>Real UID/GID</strong> → the account that launched the process.</li>
<li><strong>Effective UID/GID</strong> → what the kernel uses for permission checks. (Setuid/setgid binaries modify this.)</li>
<li><strong>Saved UID</strong> → allows a process to drop and later regain privileges (common in daemons).</li>
</ul>
<p>💡 <em>Example:</em> Apache (<code>httpd</code>) starts as <code>root</code> to bind to port 80, then immediately drops privileges to <code>www-data</code>. If the web server is compromised, the attacker only gains the limited <code>www-data</code> rights, not root.</p>
<hr>
<h3 id="why-it-matters">Why It Matters</h3>
<ul>
<li><strong>Every file belongs to someone.</strong> Delete the account, and the UID lingers.</li>
<li><strong>Every process runs as someone.</strong> If that process is compromised, its UID defines the blast radius.</li>
<li><strong>Least privilege works only if users and groups are defined properly.</strong> Services should almost never run as root.</li>
</ul>
<p>This is the backbone of Unix security. Once you grasp how file permissions and process ownership interact, the user management commands in the cheatsheet will make more sense. You’re really just moving numbers and labels around to control who owns what and who can act as whom.</p>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="dice.jpg" 
       alt="a pixel art image of a pair of 20-sided dice with a 20 and a 1 showing" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Let's roll up some users!
  </figcaption>
</figure>
<h2 id="cheatsheet">Cheatsheet</h2>
<p>Task-based, cross-platform, and hopefully complete. Each section expands with commands from basic to advanced.</p>
<hr>
<h3 id="inspect-users--groups"><span style="color:#CC0000;">Inspect Users &amp; Groups</span></h3>
<p>Check who you are, who exists on the system, and what groups users belong to.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Current identity ===</span>
</span></span><span class="line"><span class="cl">whoami                    <span class="c1"># show effective username</span>
</span></span><span class="line"><span class="cl">id                        <span class="c1"># UID, GID, groups</span>
</span></span><span class="line"><span class="cl">id -u                     <span class="c1"># numeric UID only</span>
</span></span><span class="line"><span class="cl">id -g                     <span class="c1"># numeric GID only</span>
</span></span><span class="line"><span class="cl">id -nG                    <span class="c1"># group names only</span>
</span></span><span class="line"><span class="cl">id -G                     <span class="c1"># numeric GIDs only</span>
</span></span><span class="line"><span class="cl">groups                    <span class="c1"># list groups (Linux, BSD)</span>
</span></span><span class="line"><span class="cl">groups alice              <span class="c1"># groups for another user</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === System-wide queries ===</span>
</span></span><span class="line"><span class="cl">getent passwd             <span class="c1"># all users (NSS-aware: local, LDAP, AD)</span>
</span></span><span class="line"><span class="cl">getent passwd alice       <span class="c1"># one user entry</span>
</span></span><span class="line"><span class="cl">getent group              <span class="c1"># all groups</span>
</span></span><span class="line"><span class="cl">getent group sudo         <span class="c1"># details of one group</span>
</span></span><span class="line"><span class="cl">getent group <span class="p">|</span> grep alice <span class="c1"># all groups containing alice</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Local file lookups (local-only, not NSS-aware) ===</span>
</span></span><span class="line"><span class="cl">cut -d: -f1 /etc/passwd   <span class="c1"># usernames only</span>
</span></span><span class="line"><span class="cl">awk -F: <span class="s1">&#39;$3 &lt; 1000 {print $1, $3}&#39;</span> /etc/passwd   <span class="c1"># system accounts</span>
</span></span><span class="line"><span class="cl">awk -F: <span class="s1">&#39;$3 &gt;= 1000 {print $1, $3}&#39;</span> /etc/passwd  <span class="c1"># human accounts</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS Directory Services ===</span>
</span></span><span class="line"><span class="cl">dscl . -list /Users             <span class="c1"># all users</span>
</span></span><span class="line"><span class="cl">dscl . -read /Users/alice       <span class="c1"># full record for &#39;alice&#39;</span>
</span></span><span class="line"><span class="cl">id alice                        <span class="c1"># UID/GID/groups still works</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD variants ===</span>
</span></span><span class="line"><span class="cl">pw usershow alice               <span class="c1"># FreeBSD: show one user</span>
</span></span><span class="line"><span class="cl">pw groupshow wheel              <span class="c1"># FreeBSD: show one group</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Prefer <code>id -nG</code> and <code>getent</code> in scripts (NSS-aware).</li>
<li><code>/etc/passwd</code> shows local users only, which may miss LDAP/AD accounts.</li>
<li>On macOS, <code>dscl</code> is the source of truth.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="create-users"><span style="color:#CC0000;">Create Users</span></h3>
<p>Create human accounts or service accounts.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux (Debian/Ubuntu) ===</span>
</span></span><span class="line"><span class="cl">sudo adduser alice                  <span class="c1"># interactive, sets password, creates home, shell</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Linux (RHEL/Fedora) ===</span>
</span></span><span class="line"><span class="cl">sudo useradd -m -s /bin/bash alice  <span class="c1"># create with home and shell</span>
</span></span><span class="line"><span class="cl">sudo passwd alice                   <span class="c1"># set password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Service/system accounts ===</span>
</span></span><span class="line"><span class="cl">sudo useradd -r -s /usr/sbin/nologin -d /var/www www-data
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS 10.13+ ===</span>
</span></span><span class="line"><span class="cl">sudo sysadminctl -addUser alice -fullName <span class="s2">&#34;Alice Smith&#34;</span> -password -
</span></span><span class="line"><span class="cl"><span class="c1"># Legacy alternative:</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice UserShell /bin/zsh
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice RealName <span class="s2">&#34;Alice Smith&#34;</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice UniqueID <span class="s2">&#34;501&#34;</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice PrimaryGroupID <span class="m">20</span>
</span></span><span class="line"><span class="cl">sudo dscl . -create /Users/alice NFSHomeDirectory /Users/alice
</span></span><span class="line"><span class="cl">sudo dscl . -passwd /Users/alice password</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>adduser</code> (Debian) is friendlier than <code>useradd</code> (RHEL).</li>
<li>Always use <code>-m</code> with <code>useradd</code> to ensure a home directory is created.</li>
<li>On macOS, <code>sysadminctl</code> is preferred, but <code>dscl</code> gives more fine-grained control.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="modify-users"><span style="color:#CC0000;">Modify Users</span></h3>
<p>Change passwords, shells, groups, or lock accounts.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Passwords ===</span>
</span></span><span class="line"><span class="cl">passwd alice                <span class="c1"># change password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Shell ===</span>
</span></span><span class="line"><span class="cl">chsh -s /bin/zsh alice      <span class="c1"># change login shell</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Groups ===</span>
</span></span><span class="line"><span class="cl">usermod -aG sudo alice      <span class="c1"># add alice to group (Linux)</span>
</span></span><span class="line"><span class="cl">gpasswd -a alice developers <span class="c1"># alternative on some distros</span>
</span></span><span class="line"><span class="cl">gpasswd -d alice developers <span class="c1"># remove from group</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Lock/Unlock ===</span>
</span></span><span class="line"><span class="cl">usermod -L alice            <span class="c1"># lock account (Linux)</span>
</span></span><span class="line"><span class="cl">usermod -U alice            <span class="c1"># unlock account (Linux)</span>
</span></span><span class="line"><span class="cl">passwd -l alice             <span class="c1"># lock via passwd tool</span>
</span></span><span class="line"><span class="cl">passwd -u alice             <span class="c1"># unlock</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS ===</span>
</span></span><span class="line"><span class="cl">dscl . -change /Users/alice UserShell /bin/bash /bin/zsh  <span class="c1"># only works if shell is *currently* `/bin/bash`</span>
</span></span><span class="line"><span class="cl">dscl . -append /Groups/admin GroupMembership alice</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>⚠️ <code>usermod -G</code> <strong>without <code>-a</code></strong> replaces all groups.</li>
<li>Locking prepends <code>!</code> to the shadow password field.</li>
<li>macOS uses <code>dscl</code> to edit user attributes and groups.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="delete-users"><span style="color:#CC0000;">Delete Users</span></h3>
<p>Remove accounts and clean up files.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux ===</span>
</span></span><span class="line"><span class="cl">sudo userdel alice               <span class="c1"># delete user, keep files</span>
</span></span><span class="line"><span class="cl">sudo userdel -r alice            <span class="c1"># delete user and home directory</span>
</span></span><span class="line"><span class="cl">sudo deluser alice               <span class="c1"># Debian helper</span>
</span></span><span class="line"><span class="cl">sudo deluser --remove-home alice <span class="c1"># remove home too</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Find orphaned files ===</span>
</span></span><span class="line"><span class="cl">find / -nouser -o -nogroup 2&gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS ===</span>
</span></span><span class="line"><span class="cl">sudo sysadminctl -deleteUser alice
</span></span><span class="line"><span class="cl"><span class="c1"># Or, with dscl:</span>
</span></span><span class="line"><span class="cl">sudo dscl . -delete /Users/alice</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Orphaned files will show as owned by a numeric UID.</li>
<li>Always search for <code>-nouser</code> files after deletion.</li>
<li>On macOS, <code>sysadminctl</code> handles home directory cleanup.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="manage-groups"><span style="color:#CC0000;">Manage Groups</span></h3>
<p>Groups define shared access.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux ===</span>
</span></span><span class="line"><span class="cl">groupadd developers           <span class="c1"># create group</span>
</span></span><span class="line"><span class="cl">groupdel developers           <span class="c1"># delete group</span>
</span></span><span class="line"><span class="cl">usermod -aG developers alice  <span class="c1"># add to group</span>
</span></span><span class="line"><span class="cl">gpasswd -d alice developers   <span class="c1"># remove from group</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD ===</span>
</span></span><span class="line"><span class="cl">pw groupadd developers
</span></span><span class="line"><span class="cl">pw groupmod developers -m alice
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS ===</span>
</span></span><span class="line"><span class="cl">dscl . -create /Groups/devs
</span></span><span class="line"><span class="cl">dscl . -append /Groups/devs GroupMembership alice
</span></span><span class="line"><span class="cl">dscl . -delete /Groups/devs</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Common admin groups: <code>sudo</code> (Ubuntu), <code>wheel</code> (RHEL/BSD), <code>admin</code> (macOS).</li>
<li>Setgid bit on directories (<code>chmod g+s</code>) makes files inherit group ownership.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="classic-multiuser-tools"><span style="color:#CC0000;">Classic Multiuser Tools</span></h3>
<p>Old-school, but still around.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">who              <span class="c1"># list logged-in users</span>
</span></span><span class="line"><span class="cl">w                <span class="c1"># list logged-in users + what they’re doing</span>
</span></span><span class="line"><span class="cl">users            <span class="c1"># just usernames</span>
</span></span><span class="line"><span class="cl">last             <span class="c1"># login history</span>
</span></span><span class="line"><span class="cl">write bob        <span class="c1"># message another user</span>
</span></span><span class="line"><span class="cl">wall <span class="s2">&#34;msg&#34;</span>       <span class="c1"># broadcast to all users</span>
</span></span><span class="line"><span class="cl">talk bob         <span class="c1"># split-screen chat</span>
</span></span><span class="line"><span class="cl">finger bob       <span class="c1"># show user info (if finger service enabled)</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>These tools reflect the <strong>multiuser roots</strong> of Unix — a reminder that one<br>
machine often served an entire lab or office.</li>
<li>Still useful today for <strong>audits, troubleshooting, or curiosity</strong> (e.g.<br>
spotting a forgotten session or checking login history).</li>
<li>Messaging commands like <code>write</code>, <code>wall</code>, and <code>talk</code> are often disabled on<br>
modern systems, and <code>finger</code> is usually missing entirely due to security<br>
concerns.</li>
<li>The <code>finger</code> command would also display a user’s <code>~/.plan</code> file — a personal<br>
status note people used for anything from office hours to quirky quotes.<br>
In the early internet, <code>.plan</code> files became a proto–status update, years<br>
before blogs or Twitter.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="sudo--visudo"><span style="color:#CC0000;">Sudo &amp; visudo</span></h3>
<p>The <code>/etc/sudoers</code> file decides who can act as root (or another user).<br>
Always use <code>visudo</code> to edit it — it locks the file and checks syntax so you don’t brick sudo.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Change default editor (defaults to vi) ===</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">EDITOR</span><span class="o">=</span>nano
</span></span><span class="line"><span class="cl">sudo visudo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Rule format ===</span>
</span></span><span class="line"><span class="cl">user_or_%group   <span class="nv">host</span> <span class="o">=</span> <span class="o">(</span>run_as<span class="o">)</span> command_list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Parts of a rule:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - user_or_%group → single user (alice) or group (%wheel)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - host           → usually ALL unless restricted to specific hosts</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - run_as         → ALL (default root) or another user (postgres, deploy)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - command_list   → full path(s) to allowed commands</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Open and validate sudoers ===</span>
</span></span><span class="line"><span class="cl">sudo visudo            <span class="c1"># edit main sudoers file safely</span>
</span></span><span class="line"><span class="cl">sudo visudo -c         <span class="c1"># check config syntax only</span>
</span></span><span class="line"><span class="cl">sudo visudo -f /etc/sudoers.d/webadmins   <span class="c1"># edit a drop-in file</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Give one user full root powers ===</span>
</span></span><span class="line"><span class="cl">alice <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Group-based full access ===</span>
</span></span><span class="line"><span class="cl">%wheel <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL      <span class="c1"># common on RHEL/BSD</span>
</span></span><span class="line"><span class="cl">%sudo  <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL      <span class="c1"># common on Debian/Ubuntu</span>
</span></span><span class="line"><span class="cl">%admin <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL      <span class="c1"># common on macOS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Limit bob to restarting nginx only ===</span>
</span></span><span class="line"><span class="cl">bob <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/systemctl restart nginx
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Let webadmins group manage nginx + apache ===</span>
</span></span><span class="line"><span class="cl">%webadmins <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/systemctl restart nginx, <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>                     /usr/bin/systemctl restart apache2
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Allow package updates only ===</span>
</span></span><span class="line"><span class="cl">dave <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/apt update, /usr/bin/apt upgrade
</span></span><span class="line"><span class="cl">dave <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/yum update
</span></span><span class="line"><span class="cl">dave <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/dnf upgrade
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Run Docker commands without full root ===</span>
</span></span><span class="line"><span class="cl">carol <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> /usr/bin/docker ps, /usr/bin/docker restart *
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Run commands as a different user (deploy) ===</span>
</span></span><span class="line"><span class="cl">carol <span class="nv">ALL</span><span class="o">=(</span>deploy<span class="o">)</span> /usr/bin/git pull, /usr/bin/git checkout
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Force password every time (no caching) ===</span>
</span></span><span class="line"><span class="cl">Defaults <span class="nv">timestamp_timeout</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Allow passwordless sudo (⚠️ dangerous) ===</span>
</span></span><span class="line"><span class="cl">alice <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> NOPASSWD: ALL</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Prefer drop-in configs under <code>/etc/sudoers.d/</code> instead of cluttering <code>/etc/sudoers</code>.</li>
<li>Always use <strong>absolute paths</strong> to commands in rules (<code>which systemctl</code>).</li>
<li>Use groups (<code>%group</code>) to manage privileges cleanly for teams.</li>
<li>Check syntax anytime with <code>sudo visudo -c</code>.</li>
</ul>
<p>🔗 <strong>Docs &amp; References:</strong></p>
<ul>
<li><a href="https://www.sudo.ws/docs/man/sudoers.man/">man 5 sudoers</a></li>
<li><a href="https://www.sudo.ws/docs/man/sudoers.man/">Sudoers Manual</a></li>
<li><a href="https://wiki.archlinux.org/title/sudo">ArchWiki: Sudo</a></li>
</ul>

  </div>
</details>

<hr>
<h3 id="troubleshooting"><span style="color:#CC0000;">Troubleshooting</span></h3>
<p>When a user can’t log in, can’t write files, or <code>sudo</code> mysteriously fails, these checks will save you.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === File permission issues ===</span>
</span></span><span class="line"><span class="cl">ls -l file                  <span class="c1"># check ownership + rwx bits</span>
</span></span><span class="line"><span class="cl">id alice                    <span class="c1"># confirm UID + GIDs</span>
</span></span><span class="line"><span class="cl">groups alice                <span class="c1"># confirm group memberships</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Login problems ===</span>
</span></span><span class="line"><span class="cl">passwd -S alice             <span class="c1"># Linux: check password status (L=locked, P=usable)</span>
</span></span><span class="line"><span class="cl">chage -l alice              <span class="c1"># Linux: check password aging + expiry</span>
</span></span><span class="line"><span class="cl">grep ^alice: /etc/passwd    <span class="c1"># check home dir + shell field</span>
</span></span><span class="line"><span class="cl">dscl . -read /Users/alice   <span class="c1"># macOS: inspect account record</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Orphaned files or groups ===</span>
</span></span><span class="line"><span class="cl">find / -nouser -o -nogroup 2&gt;/dev/null   <span class="c1"># files with no matching UID/GID</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Process ownership ===</span>
</span></span><span class="line"><span class="cl">ps -u alice                 <span class="c1"># all processes owned by alice</span>
</span></span><span class="line"><span class="cl">pgrep -u alice              <span class="c1"># list PIDs only</span>
</span></span><span class="line"><span class="cl">pkill -u alice              <span class="c1"># kill all processes for alice (⚠️ destructive)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Sudo debugging ===</span>
</span></span><span class="line"><span class="cl">sudo -l                     <span class="c1"># list sudo rights for current user</span>
</span></span><span class="line"><span class="cl">sudo -v                     <span class="c1"># refresh credentials (prompts password)</span>
</span></span><span class="line"><span class="cl">sudo -k                     <span class="c1"># expire cached credentials</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>If <code>/etc/passwd</code> shows <code>/usr/sbin/nologin</code>, <code>/sbin/nologin</code>, or <code>/bin/false</code>, the user cannot log in interactively.</li>
<li>Account expiry or locks often explain mysterious login failures (<code>passwd -S</code>, <code>chage -l</code>).</li>
<li>Always check group membership (<code>id -nG</code>) when file access doesn’t make sense.</li>
<li>On macOS, many system accounts start with <code>_</code> and are not intended for login.</li>
</ul>
  </div>
</details>

<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="orcs.jpg" 
       alt="a pixel art image of 3 orcs armed with spears and swords in a dungeon setting" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Onward to advanced topics — time to deal with pesky orcs.  
  </figcaption>
</figure>
<h2 id="advanced-topics">Advanced Topics</h2>
<hr>
<h3 id="defaults--templates"><span style="color:#CC0000;">Defaults &amp; Templates</span></h3>
<p>When a new account is created, the system applies defaults: skeleton files, UID ranges, shells, and the <strong>default mask</strong> (<code>umask</code>). These define how a fresh user’s environment looks and how secure their files are.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Skeleton files ===</span>
</span></span><span class="line"><span class="cl">ls -A /etc/skel          <span class="c1"># files copied into new home dirs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># .bashrc  .profile  .bash_logout</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a custom file for all new users</span>
</span></span><span class="line"><span class="cl">sudo cp /etc/motd /etc/skel/welcome.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Default umask ===</span>
</span></span><span class="line"><span class="cl"><span class="nb">umask</span>                    <span class="c1"># show current mask</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 0022 → new files 644 (-rw-r--r--) and dirs 755 (drwxr-xr-x)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change umask for tighter privacy (per shell session)</span>
</span></span><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">0077</span>
</span></span><span class="line"><span class="cl">touch secret.txt <span class="o">&amp;&amp;</span> ls -l secret.txt
</span></span><span class="line"><span class="cl"><span class="c1"># -rw------- 1 alice users 0 Sep 21 16:10 secret.txt</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Global login defaults (Linux) ===</span>
</span></span><span class="line"><span class="cl">grep -E <span class="s1">&#39;UID_MIN|UID_MAX|GID_MIN|GID_MAX&#39;</span> /etc/login.defs
</span></span><span class="line"><span class="cl"><span class="c1"># UID_MIN 1000, UID_MAX 60000</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>How <code>/etc/skel</code> works:</strong></p>
<ul>
<li>Any file or directory in <code>/etc/skel</code> gets copied into the new user’s home when the account is created (unless disabled with <code>useradd -M -k</code>).</li>
<li>Typical contents: <code>.bashrc</code>, <code>.profile</code>, <code>.bash_logout</code>.</li>
<li>You can also include a company-wide <code>README</code>, a <code>welcome.txt</code>, or even preconfigured dotfiles like <code>.vimrc</code> or <code>.gitconfig</code>.</li>
<li>Updating <code>/etc/skel</code> only affects <em>future</em> accounts, not existing ones.</li>
</ul>
</li>
<li>
<p><strong>How <code>umask</code> works:</strong></p>
<ul>
<li>It’s a <strong>subtractive mask</strong>: permissions are removed from the base defaults (666 for files, 777 for directories).</li>
<li>Example:
<ul>
<li><code>umask 0022</code> → files 644 (<code>rw-r--r--</code>), dirs 755 (<code>rwxr-xr-x</code>) → standard, readable by everyone.</li>
<li><code>umask 0077</code> → files 600 (<code>rw-------</code>), dirs 700 (<code>rwx------</code>) → private, nobody else can read.</li>
<li><code>umask 0002</code> → files 664, dirs 775 → collaborative group environments.</li>
</ul>
</li>
<li>Why change it?
<ul>
<li>Servers often use <code>0022</code> (safe default).</li>
<li>Multiuser/dev environments may prefer <code>0002</code> so teams in the same group can share files easily.</li>
<li>Security-sensitive environments often use <code>0077</code> to prevent accidental leakage.</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Other considerations:</strong></p>
<ul>
<li><code>umask</code> can differ between shells, cron jobs, and systemd services.</li>
<li>Linux: defaults can be set in <code>/etc/login.defs</code> or PAM config.</li>
<li>BSD: see <code>/etc/adduser.conf</code>.</li>
<li>macOS: relies on <code>sysadminctl</code> + Directory Services defaults.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="name-service-switch--domains"><span style="color:#CC0000;">Name Service Switch &amp; Domains</span></h3>
<p>On modern systems, <code>/etc/passwd</code> is just one source of truth. Enterprises often keep users in <strong>LDAP</strong>, <strong>Kerberos realms</strong>, or <strong>Active Directory</strong>. The <strong>Name Service Switch (NSS)</strong> decides where the system looks when resolving usernames, groups, and hosts.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === NSS lookup order (Linux) ===</span>
</span></span><span class="line"><span class="cl">grep passwd /etc/nsswitch.conf
</span></span><span class="line"><span class="cl"><span class="c1"># passwd: files systemd sss</span>
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;files&#34; = /etc/passwd, &#34;systemd&#34; = local systemd users, &#34;sss&#34; = SSSD (LDAP/AD)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Query via NSS (all backends) ===</span>
</span></span><span class="line"><span class="cl">getent passwd alice          <span class="c1"># works even if alice is in LDAP/AD</span>
</span></span><span class="line"><span class="cl">getent group devs            <span class="c1"># query group membership</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Show only local file (not NSS aware) ===</span>
</span></span><span class="line"><span class="cl">cat /etc/passwd <span class="p">|</span> grep alice <span class="c1"># will miss LDAP/AD users</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Join an AD domain (Linux, via realmd/SSSD) ===</span>
</span></span><span class="line"><span class="cl">realm discover example.com   <span class="c1"># discover domain controllers</span>
</span></span><span class="line"><span class="cl">sudo realm join example.com  <span class="c1"># join domain</span>
</span></span><span class="line"><span class="cl">systemctl status sssd        <span class="c1"># domain users now available via SSSD</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Test a domain account ===</span>
</span></span><span class="line"><span class="cl">id alice@example.com
</span></span><span class="line"><span class="cl"><span class="c1"># uid=123456789(alice@example.com) gid=123456789(domain users) groups=...</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>NSS vs PAM:</strong></p>
<ul>
<li><strong>NSS</strong> answers “does this identity exist?” (user/group lookups).</li>
<li><strong>PAM</strong> answers “can this identity log in?” (authentication, password policy, session rules).</li>
</ul>
</li>
<li>
<p><strong>Why <code>getent</code> matters:</strong></p>
<ul>
<li><code>getent</code> queries the entire NSS stack — so LDAP, AD, or other remote backends are included.</li>
<li><code>cat /etc/passwd</code> only shows local users and will <em>miss</em> network accounts.</li>
</ul>
</li>
<li>
<p><strong>SSSD &amp; caching:</strong></p>
<ul>
<li>On Linux, <strong>SSSD</strong> (System Security Services Daemon) acts as the glue for LDAP/AD lookups.</li>
<li>It caches users for performance and offline login.</li>
<li>If users/groups seem stale, clear the cache:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sss_cache -E
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Joining domains:</strong></p>
<ul>
<li><code>realmd</code> + <code>sssd</code> is the common modern combo (RHEL, Fedora, Ubuntu).</li>
<li>Older setups may use <strong>nslcd</strong> or <strong>winbind</strong> for LDAP/AD.</li>
<li>macOS has its own directory service integration (<code>dsconfigad</code>).</li>
<li>BSD systems typically rely on <code>nss_ldap</code> + <code>pam_ldap</code>.</li>
</ul>
</li>
<li>
<p><strong>Troubleshooting tip:</strong></p>
<ul>
<li>Always check <code>nsswitch.conf</code> first — if “sss” or “ldap” isn’t listed for <code>passwd</code> and <code>group</code>, your system won’t even <em>try</em> querying the domain.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="access-control-lists-acls"><span style="color:#CC0000;">Access Control Lists (ACLs)</span></h3>
<p>The classic Unix model (user/group/other) is simple but limited. What if you want <strong>multiple users</strong> with different rights on the same file, without changing ownership or creating new groups? That’s where <strong>Access Control Lists (ACLs)</strong> come in. ACLs add fine-grained permissions on top of the traditional model.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === See current permissions (no ACL yet) ===</span>
</span></span><span class="line"><span class="cl">ls -l project.txt
</span></span><span class="line"><span class="cl"><span class="c1"># -rw-r----- 1 alice devs 42 Sep 21 17:00 project.txt</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Only alice (owner) has rw, devs group has r, others none.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Add bob with RW access via ACL ===</span>
</span></span><span class="line"><span class="cl">setfacl -m u:bob:rw project.txt
</span></span><span class="line"><span class="cl">getfacl project.txt
</span></span><span class="line"><span class="cl"><span class="c1"># file: project.txt</span>
</span></span><span class="line"><span class="cl"><span class="c1"># owner: alice</span>
</span></span><span class="line"><span class="cl"><span class="c1"># group: devs</span>
</span></span><span class="line"><span class="cl">user::rw-
</span></span><span class="line"><span class="cl">user:bob:rw-          <span class="c1"># new ACL entry</span>
</span></span><span class="line"><span class="cl">group::r--
</span></span><span class="line"><span class="cl">mask::rw-
</span></span><span class="line"><span class="cl">other::---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Remove bob&#39;s entry later ===</span>
</span></span><span class="line"><span class="cl">setfacl -x u:bob project.txt
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Default ACL on a directory ===</span>
</span></span><span class="line"><span class="cl">setfacl -d -m g:devs:rwx /srv/project
</span></span><span class="line"><span class="cl">ls -ld /srv/project
</span></span><span class="line"><span class="cl"><span class="c1"># drwxrwxr-x+ 2 root root 4096 Sep 21 17:05 /srv/project</span>
</span></span><span class="line"><span class="cl"><span class="c1"># (+ indicates ACLs are set)</span>
</span></span><span class="line"><span class="cl">getfacl /srv/project
</span></span><span class="line"><span class="cl"><span class="c1"># default:group:devs:rwx</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now any file created under /srv/project inherits group rwx.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS NFSv4 ACLs ===</span>
</span></span><span class="line"><span class="cl">ls -le project.txt
</span></span><span class="line"><span class="cl">-rw-r-----+ <span class="m">1</span> alice staff <span class="m">42</span> Sep <span class="m">21</span> 17:10 project.txt
</span></span><span class="line"><span class="cl"> 0: user:bob allow read,write
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">chmod +a <span class="s2">&#34;bob allow read,write&#34;</span> project.txt</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Why ACLs?</strong></p>
<ul>
<li>u/g/o model works fine until you need to share a resource between <em>specific users</em> who don’t share a group.</li>
<li>ACLs let you grant per-user or per-group rights without redesigning ownership.</li>
<li>Great for project directories, shared data, or complex multiuser environments.</li>
</ul>
</li>
<li>
<p><strong>Default ACLs:</strong></p>
<ul>
<li>On directories, default ACLs ensure all <em>new files</em> inside inherit the access rules automatically.</li>
<li>Example: shared group workspaces, where all new files should be writable by <code>devs</code>.</li>
</ul>
</li>
<li>
<p><strong>Implementation differences:</strong></p>
<ul>
<li>Linux &amp; BSD: <code>setfacl</code>, <code>getfacl</code>.</li>
<li>macOS: NFSv4 ACLs with <code>ls -le</code> and <code>chmod +a</code>.</li>
<li>The <code>+</code> sign in <code>ls -l</code> output means “this file has ACLs.”</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Not all filesystems support ACLs (may need <code>mount -o acl</code> on ext4).</li>
<li>Backups that don’t preserve extended attributes may strip ACLs.</li>
<li>ACLs can make permissions confusing — always use <code>getfacl</code> to confirm.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="linux-capabilities"><span style="color:#CC0000;">Linux Capabilities</span></h3>
<p>Traditionally, if a program needed <em>any</em> privileged action (like opening a raw socket or binding to a low port), it had to be setuid root. That gave it <strong>full root power</strong>, even if it only needed one small permission.</p>
<p><strong>Linux capabilities</strong> break root’s powers into fine-grained units — like <code>CAP_NET_RAW</code> (raw sockets) or <code>CAP_SYS_ADMIN</code> (system-wide admin). You can then grant a binary <em>just the slice it needs</em> instead of all-or-nothing root.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Traditional setuid ping ===</span>
</span></span><span class="line"><span class="cl">ls -l /bin/ping
</span></span><span class="line"><span class="cl"><span class="c1"># -rwsr-xr-x 1 root root ...</span>
</span></span><span class="line"><span class="cl"><span class="c1"># setuid root: ping runs as full root just to open raw sockets.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Replace with capability ===</span>
</span></span><span class="line"><span class="cl">sudo chmod u-s /bin/ping                   <span class="c1"># remove setuid bit</span>
</span></span><span class="line"><span class="cl">sudo setcap cap_net_raw+ep /bin/ping       <span class="c1"># give only raw socket ability</span>
</span></span><span class="line"><span class="cl">getcap /bin/ping
</span></span><span class="line"><span class="cl"><span class="c1"># /bin/ping = cap_net_raw+ep</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Test that it works ===</span>
</span></span><span class="line"><span class="cl">ping -c1 127.0.0.1                         <span class="c1"># works without setuid</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Drop capabilities from a running process (demo with sleep) ===</span>
</span></span><span class="line"><span class="cl">sleep <span class="m">100</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="nv">pid</span><span class="o">=</span><span class="nv">$!</span>
</span></span><span class="line"><span class="cl">grep CapEff /proc/<span class="nv">$pid</span>/status              <span class="c1"># shows effective caps (usually 0000000000000000)</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Safer than setuid:</strong></p>
<ul>
<li>Old way: <code>ping</code> had full root rights — if exploited, attacker gets root.</li>
<li>With capabilities: attacker only gains the one granted privilege.</li>
</ul>
</li>
<li>
<p><strong>How capabilities are assigned:</strong></p>
<ul>
<li><code>setcap cap_name+ep file</code> → give a binary capability (<code>e=effective</code>, <code>p=permitted</code>).</li>
<li><code>getcap file</code> → check capabilities.</li>
<li><code>capsh --print</code> → view current shell’s capabilities.</li>
</ul>
</li>
<li>
<p><strong>Common useful capabilities:</strong></p>
<ul>
<li><code>CAP_NET_BIND_SERVICE</code> → bind to ports &lt;1024 without root.</li>
<li><code>CAP_NET_ADMIN</code> → manage networking.</li>
<li><code>CAP_SYS_TIME</code> → set the system clock.</li>
<li><code>CAP_SYS_ADMIN</code> → (⚠️ extremely broad, “root-lite”).</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Capabilities are stored as extended attributes — they don’t survive a normal <code>cp</code>. Use <code>rsync -aX</code> or <code>install -m755 -o root -g root</code>.</li>
<li>Some filesystems don’t support extended attributes (e.g. older NFS).</li>
<li>Granting too many caps (especially <code>CAP_SYS_ADMIN</code>) defeats the purpose.</li>
</ul>
</li>
<li>
<p><strong>Why care?</strong></p>
<ul>
<li>Capabilities let you follow <em>least privilege</em> in service design.</li>
<li>Systemd services can also drop or restrict capabilities with <code>CapabilityBoundingSet=</code> in unit files.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="systemd-dynamic-users"><span style="color:#CC0000;">Systemd Dynamic Users</span></h3>
<p>Normally, services run under pre-created system accounts like <code>www-data</code> or <code>mysql</code>. But that clutters <code>/etc/passwd</code> with dozens of long-lived identities that stick around even if the service is removed.</p>
<p><strong>Dynamic users</strong> solve this: systemd can allocate a <strong>throwaway UID/GID at runtime</strong> when the service starts. When the service stops, the UID disappears. It’s perfect for daemons that don’t need persistent files or shells.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/web.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/mydaemon</span>
</span></span><span class="line"><span class="cl"><span class="na">DynamicUser</span><span class="o">=</span><span class="s">yes                    # allocate ephemeral UID at runtime</span>
</span></span><span class="line"><span class="cl"><span class="na">CapabilityBoundingSet</span><span class="o">=</span><span class="s">CAP_NET_BIND_SERVICE</span>
</span></span><span class="line"><span class="cl"><span class="na">ProtectSystem</span><span class="o">=</span><span class="s">strict</span>
</span></span><span class="line"><span class="cl"><span class="na">ProtectHome</span><span class="o">=</span><span class="s">yes</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateTmp</span><span class="o">=</span><span class="s">yes</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>How it works:</strong></p>
<ul>
<li>Each start assigns a UID like <code>dynamic-1000</code>.</li>
<li>No entry is written to <code>/etc/passwd</code>; it’s entirely managed by systemd.</li>
<li>The UID disappears once the service stops.</li>
</ul>
</li>
<li>
<p><strong>When to use:</strong></p>
<ul>
<li>For services that don’t need a home directory or persistent files.</li>
<li>Great for stateless daemons, network listeners, or sandboxed apps.</li>
</ul>
</li>
<li>
<p><strong>Persistent data:</strong></p>
<ul>
<li>Use <code>StateDirectory=</code>, <code>CacheDirectory=</code>, or <code>LogsDirectory=</code> in the unit file to create system-managed dirs with correct ownership.</li>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">StateDirectory</span><span class="o">=</span><span class="s">mydaemon</span>
</span></span></code></pre></div>→ systemd creates <code>/var/lib/mydaemon/</code> owned by the dynamic UID.</li>
</ul>
</li>
<li>
<p><strong>Security hardening:</strong></p>
<ul>
<li>Combine <code>DynamicUser=yes</code> with:
<ul>
<li><code>ProtectSystem=strict</code> → service sees <code>/usr</code> as read-only.</li>
<li><code>ProtectHome=yes</code> → blocks access to <code>/home</code>.</li>
<li><code>PrivateTmp=yes</code> → gives the service its own <code>/tmp</code>.</li>
<li><code>NoNewPrivileges=yes</code> → prevents privilege escalation.</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>No permanent account entry → you can’t <code>su</code> or <code>ssh</code> into it.</li>
<li>UIDs are reused; you can’t rely on the number being the same between runs.</li>
<li>If the service needs to write to disk, you <em>must</em> use the <code>StateDirectory</code>/<code>CacheDirectory</code> approach, or files will be inaccessible.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="rootless-containers--subuids"><span style="color:#CC0000;">Rootless Containers &amp; Subuids</span></h3>
<p>Normally, containers run as <code>root</code>, which maps directly to the host’s root — a big risk if the container is compromised. Rootless containers avoid this by <strong>mapping container UIDs/GIDs to high-numbered “subuids” and “subgids” on the host</strong>.</p>
<p>That way, <code>root</code> inside the container is really just UID 100000+ on the host — isolated, unprivileged, and unable to harm the real system.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Show subuid/subgid allocation ===</span>
</span></span><span class="line"><span class="cl">grep alice /etc/subuid /etc/subgid
</span></span><span class="line"><span class="cl"><span class="c1"># alice:100000:65536</span>
</span></span><span class="line"><span class="cl"><span class="c1"># means: user &#39;alice&#39; gets a block of 65,536 UIDs/GIDs starting at 100000.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Run a rootless container ===</span>
</span></span><span class="line"><span class="cl">podman run --rm alpine id
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From inside the container it looks like root,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># but on the host it&#39;s actually UID 100000+ from /etc/subuid.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Inspect namespace mapping ===</span>
</span></span><span class="line"><span class="cl">podman unshare cat /proc/self/uid_map
</span></span><span class="line"><span class="cl"><span class="c1"># 0 100000 65536</span>
</span></span><span class="line"><span class="cl"><span class="c1"># &#34;container UID 0 maps to host UID 100000, size 65536&#34;</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>How it works:</strong></p>
<ul>
<li><code>/etc/subuid</code> and <code>/etc/subgid</code> allocate “ranges” of host IDs to a user.</li>
<li>When a rootless container starts, container UID 0 → host UID 100000, container UID 1 → host UID 100001, etc.</li>
<li>This mapping isolates container processes from the host.</li>
</ul>
</li>
<li>
<p><strong>Why it matters:</strong></p>
<ul>
<li>Running as root in a container no longer equals root on the host.</li>
<li>Even if the container is compromised, the attacker only controls high-numbered, unprivileged UIDs.</li>
<li>This is how tools like <strong>Podman</strong>, <strong>Buildah</strong>, and rootless <strong>Docker</strong> enforce least privilege.</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Without entries in <code>/etc/subuid</code> and <code>/etc/subgid</code>, rootless containers fail to start.</li>
<li>Each user gets ~65k IDs by default; this can be adjusted in <code>/etc/subuid</code>.</li>
<li>Requires <strong>user namespaces</strong> in the kernel (<code>CONFIG_USER_NS=y</code>).</li>
<li>Files created by container processes will show up on the host as UID 100000+, which can look odd in <code>ls -l</code>.</li>
</ul>
</li>
<li>
<p><strong>Related commands:</strong></p>
<ul>
<li><code>podman unshare</code> → enter the container’s user namespace for debugging.</li>
<li><code>newuidmap</code> / <code>newgidmap</code> → helper programs to set up ID ranges.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="password-policy--lockouts"><span style="color:#CC0000;">Password Policy &amp; Lockouts</span></h3>
<p>User accounts also have <strong>lifespans and safety rules</strong>.</p>
<p>Password policy defines how often a user must change their password, how complex it must be, and how many failed logins before the account locks.</p>
<p>This is enforced through <strong>shadow file aging fields</strong> (Linux/Unix), <strong>PAM modules</strong> for lockouts, and platform-specific tools on macOS and BSD.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Linux: show password aging/expiry ===</span>
</span></span><span class="line"><span class="cl">chage -l alice
</span></span><span class="line"><span class="cl"><span class="c1"># Last password change                                    : Sep 21, 2025</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Password expires                                       : Nov 20, 2025</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Password inactive                                      : never</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Account expires                                        : never</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Minimum number of days between password change         : 0</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Maximum number of days between password change         : 60</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Number of days of warning before password expires      : 7</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Linux: check failed logins ===</span>
</span></span><span class="line"><span class="cl">faillock --user alice
</span></span><span class="line"><span class="cl"><span class="c1"># alice:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># When        Type  Source</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2025-09-21  TTY   ssh:notty</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2025-09-21  TTY   ssh:notty</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Lock out after 3 failures (RHEL/Ubuntu with pam_faillock)</span>
</span></span><span class="line"><span class="cl">sudo faillock --setdeny<span class="o">=</span><span class="m">3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD: enforce minimum password age ===</span>
</span></span><span class="line"><span class="cl">passwd -n <span class="m">30</span> alice     <span class="c1"># must wait 30 days before changing again</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === macOS: show password policy ===</span>
</span></span><span class="line"><span class="cl">pwpolicy -u alice -getpolicy
</span></span><span class="line"><span class="cl"><span class="c1"># prints dictionary of rules:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># usingHistory=15 minChars=8 requiresMixedCase=1 requiresNumeric=1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set stricter policy on macOS</span>
</span></span><span class="line"><span class="cl">sudo pwpolicy -u alice -setpolicy <span class="s2">&#34;minChars=12 requiresMixedCase=1 requiresNumeric=1 requiresSymbol=1&#34;</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Why it matters:</strong></p>
<ul>
<li>Password aging prevents accounts from using the same password forever.</li>
<li>Lockouts protect against brute force attacks but can cause accidental denial of service.</li>
<li>Compliance frameworks (HIPAA, PCI-DSS, etc.) often mandate specific expiry and complexity rules.</li>
</ul>
</li>
<li>
<p><strong>Linux specifics:</strong></p>
<ul>
<li><code>chage</code> edits shadow file fields directly.</li>
<li><code>faillock</code> (PAM module) counts failed attempts and locks accounts temporarily.</li>
<li>Debian historically used <code>pam_tally2</code>, but newer distros prefer <code>faillock</code>.</li>
<li>Lockouts can be reset: <code>faillock --user alice --reset</code>.</li>
</ul>
</li>
<li>
<p><strong>BSD specifics:</strong></p>
<ul>
<li><code>passwd</code> options enforce password minimum/maximum ages.</li>
<li>Some BSDs use <code>login.conf</code> for global policy.</li>
</ul>
</li>
<li>
<p><strong>macOS specifics:</strong></p>
<ul>
<li><code>pwpolicy</code> manages per-user or global rules.</li>
<li>Many system accounts (like <code>_spotlight</code>) are exempt.</li>
<li><strong>SecureToken</strong>: separate from password policy, it controls FileVault unlock ability. Losing SecureToken can lock a user out of disk encryption.</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Expired accounts often just show as “login incorrect” with no obvious hint. Always check <code>chage -l</code>.</li>
<li>Too strict a lockout policy can become a DoS if an attacker keeps intentionally failing logins.</li>
<li>Remote directory systems (LDAP/AD) often enforce their own policies that override local rules.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="audit--logs"><span style="color:#CC0000;">Audit &amp; Logs</span></h3>
<p>When a user can’t log in, sudo fails, or permissions seem wrong, the <strong>logs tell the story</strong>. Different Unix-like systems store them in different places, but the principles are the same: check authentication logs, check system journals, and look at login history.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Debian/Ubuntu: SSH &amp; sudo events ===</span>
</span></span><span class="line"><span class="cl">tail -f /var/log/auth.log
</span></span><span class="line"><span class="cl"><span class="c1"># Sep 21 17:40 server sshd[2345]: Failed password for bob from 192.168.1.20 port 55312 ssh2</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Sep 21 17:42 server sudo:   alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/bin/ls</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === RHEL/Fedora equivalents ===</span>
</span></span><span class="line"><span class="cl">tail -f /var/log/secure
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === systemd journal: filter by UID ===</span>
</span></span><span class="line"><span class="cl">journalctl <span class="nv">_UID</span><span class="o">=</span><span class="m">1000</span> --since today
</span></span><span class="line"><span class="cl"><span class="c1"># shows all messages generated by UID 1000 (alice)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === systemd journal: filter by command ===</span>
</span></span><span class="line"><span class="cl">journalctl <span class="nv">_COMM</span><span class="o">=</span>sudo -S today
</span></span><span class="line"><span class="cl"><span class="c1"># shows all sudo invocations since today</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Login history: successful ===</span>
</span></span><span class="line"><span class="cl">last
</span></span><span class="line"><span class="cl"><span class="c1"># alice   pts/0        192.168.1.20     Sun Sep 21 17:00   </span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Login history: failed ===</span>
</span></span><span class="line"><span class="cl">lastb
</span></span><span class="line"><span class="cl"><span class="c1"># bob     ssh:notty    192.168.1.20     Sun Sep 21 17:40   </span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>File locations:</strong></p>
<ul>
<li>Debian/Ubuntu → <code>/var/log/auth.log</code>.</li>
<li>RHEL/Fedora → <code>/var/log/secure</code>.</li>
<li>BSD → <code>/var/log/auth.log</code> or <code>/var/log/messages</code> depending on config.</li>
<li>macOS → <code>/var/log/asl/</code> (older) or <code>log show --predicate 'eventMessage contains &quot;sshd&quot;'</code>.</li>
</ul>
</li>
<li>
<p><strong>systemd journal tips:</strong></p>
<ul>
<li><code>_UID=1000</code> → filter logs from a specific user ID.</li>
<li><code>_COMM=sudo</code> → filter by executable name.</li>
<li><code>-S yesterday</code> / <code>--since &quot;2025-09-20 18:00&quot;</code> → time filters.</li>
<li>Enable persistence:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo mkdir -p /var/log/journal
</span></span><span class="line"><span class="cl">sudo systemctl restart systemd-journald
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Login history:</strong></p>
<ul>
<li><code>last</code> reads <code>/var/log/wtmp</code> → shows successful logins.</li>
<li><code>lastb</code> reads <code>/var/log/btmp</code> → shows failed logins (may need root to read).</li>
<li>Use <code>last -f /path/to/wtmp.old</code> to read rotated logs.</li>
</ul>
</li>
<li>
<p><strong>Why this matters:</strong></p>
<ul>
<li>Failed logins reveal brute-force attempts.</li>
<li><code>sudo</code> log entries show exactly which commands were run and by whom.</li>
<li>Filtering by UID is useful for tracing a specific account across the system.</li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Journald defaults to in-memory logs; without persistence, entries vanish after reboot.</li>
<li>Log rotation may remove history faster than expected (<code>/etc/logrotate.d/</code>).</li>
<li><code>lastb</code> output can flood if you’re under SSH brute-force attack; use <code>grep</code> to filter by username.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="ssh-key-restrictions"><span style="color:#CC0000;">SSH Key Restrictions</span></h3>
<p>SSH public keys don’t just allow or deny login — you can <strong>control what they’re allowed to do</strong>. This is especially useful for automation accounts (backups, deploy scripts, CI/CD) where you don’t want full shell access.</p>
<p>Restrictions are written in <code>~/.ssh/authorized_keys</code> before the key itself. Multiple restrictions can be combined with commas.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># === Restrict login to a specific subnet ===
</span></span><span class="line"><span class="cl">from=&#34;192.168.1.0/24&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Force a command (ignore user input) ===
</span></span><span class="line"><span class="cl">command=&#34;/usr/local/bin/backup.sh&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># When this key logs in, it *always* runs backup.sh — no shell access.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Disable shell/TTY allocation ===
</span></span><span class="line"><span class="cl">no-pty,command=&#34;/usr/bin/rsync --server --sender ...&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># Useful for file transfers only.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Combine multiple restrictions ===
</span></span><span class="line"><span class="cl">from=&#34;10.0.0.5&#34;,no-pty,command=&#34;/usr/local/bin/deploy.sh&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># Only works from 10.0.0.5, no interactive shell, forced deploy script.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># === Log key usage for auditing ===
</span></span><span class="line"><span class="cl">environment=&#34;DEPLOY_KEY_ID=ci-runner&#34; ssh-ed25519 AAAAC3Nza...
</span></span><span class="line"><span class="cl"># Adds DEPLOY_KEY_ID to environment for logging in scripts.</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>Why do this?</strong></p>
<ul>
<li>Automation accounts (backups, deployments, monitoring) don’t need full shell access.</li>
<li>Restricting keys reduces the blast radius if a key leaks.</li>
</ul>
</li>
<li>
<p><strong>Common options:</strong></p>
<ul>
<li><code>from=&quot;addrlist&quot;</code> → restrict to specific IPs or subnets.</li>
<li><code>command=&quot;cmd&quot;</code> → always run this command instead of a shell.</li>
<li><code>no-pty</code> → disables interactive sessions.</li>
<li><code>environment=&quot;VAR=value&quot;</code> → injects env vars, useful for logging or scripts.</li>
<li><code>restrict</code> (newer OpenSSH) → a safe default that implies multiple restrictions (no port forwarding, no agent, no PTY).</li>
</ul>
</li>
<li>
<p><strong>Extra hardening:</strong></p>
<ul>
<li>Combine with <code>Match User</code> or <code>Match Address</code> blocks in <code>sshd_config</code>.</li>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">Match User backup
</span></span><span class="line"><span class="cl">    ChrootDirectory /backups
</span></span><span class="line"><span class="cl">    ForceCommand /usr/local/bin/backup.sh
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Gotchas:</strong></p>
<ul>
<li>Syntax is strict — options must come <em>before</em> the key, separated by commas.</li>
<li>A single typo can prevent login.</li>
<li>Forced commands must use <strong>absolute paths</strong>.</li>
<li>Debug failures with <code>sshd -T</code> (shows effective config) and <code>ssh -vvv user@host</code>.</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<h3 id="other-os-specific-nuggets"><span style="color:#CC0000;">Other OS-Specific Nuggets</span></h3>
<p>Not every Unix-like does user management the same way. Here are a few platform-specific details that matter when you step outside Linux.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === macOS: FileVault &amp; SecureToken ===</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Check if a user has SecureToken (needed to unlock FileVault at boot)</span>
</span></span><span class="line"><span class="cl">sysadminctl -secureTokenStatus alice
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Grant FileVault unlock rights to an existing account</span>
</span></span><span class="line"><span class="cl">sudo fdesetup add -usertoadd alice
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List all FileVault-enabled users</span>
</span></span><span class="line"><span class="cl">fdesetup list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === BSD: doas instead of sudo ===</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Install and configure a simple allow rule for wheel group</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;permit :wheel&#34;</span> <span class="p">|</span> sudo tee /usr/local/etc/doas.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test it</span>
</span></span><span class="line"><span class="cl">doas whoami
</span></span><span class="line"><span class="cl"><span class="c1"># root</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Linux/systemd: user sessions ===</span>
</span></span><span class="line"><span class="cl"><span class="c1"># List all active systemd user sessions</span>
</span></span><span class="line"><span class="cl">loginctl list-users
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Show details for a single user session</span>
</span></span><span class="line"><span class="cl">loginctl user-status alice</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>
<p><strong>macOS:</strong></p>
<ul>
<li><strong>SecureToken</strong> is a flag that determines whether a user can unlock FileVault at boot.</li>
<li>Creating a new admin user does <em>not</em> automatically give it SecureToken. You may need to explicitly grant it via another SecureToken user.</li>
<li><code>fdesetup</code> controls FileVault enrollment, token grants, and recovery keys.</li>
</ul>
</li>
<li>
<p><strong>BSD (<code>doas</code>):</strong></p>
<ul>
<li><code>doas</code> is OpenBSD’s minimalist alternative to <code>sudo</code>.</li>
<li>Config lives in <code>/etc/doas.conf</code> or <code>/usr/local/etc/doas.conf</code>.</li>
<li>Syntax is simple:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">permit :wheel
</span></span><span class="line"><span class="cl">permit nopass keepenv alice as root cmd /usr/bin/pkg_add
</span></span></code></pre></div></li>
<li>Security philosophy: fewer moving parts, less chance of misconfiguration.</li>
</ul>
</li>
<li>
<p><strong>Linux/systemd (<code>loginctl</code>):</strong></p>
<ul>
<li><code>loginctl</code> shows how users map to systemd sessions. Useful for debugging lingering sessions, <code>linger</code> (allowing services to keep running after logout), and session limits.</li>
<li>Example:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">loginctl enable-linger alice   <span class="c1"># keep user services running after logout</span>
</span></span></code></pre></div></li>
<li>Helpful when running background daemons or timers under a regular user account.</li>
</ul>
</li>
<li>
<p><strong>General tip:</strong></p>
<ul>
<li>When troubleshooting across OSes, always check “what’s the local identity backend?”
<ul>
<li>macOS → Directory Services (<code>dscl</code>, <code>sysadminctl</code>).</li>
<li>BSD → <code>pw</code> / <code>doas</code>.</li>
<li>Linux → <code>systemd-logind</code>, NSS, SSSD.</li>
</ul>
</li>
</ul>
</li>
</ul>

  </div>
</details>

<hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="loot.jpg" 
       alt="pixel art image of a party of fantasy adventurers happily gathered around a treasure chest" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    If you made it this far you've earned some loot and XP!
  </figcaption>
</figure>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<h3 id="core-resources">Core Resources</h3>
<ul>
<li><a href="https://man7.org/linux/man-pages/man5/passwd.5.html">man 5 passwd</a>, <a href="https://man7.org/linux/man-pages/man5/shadow.5.html">man 5 shadow</a>, <a href="https://man7.org/linux/man-pages/man5/group.5.html">man 5 group</a>, <a href="https://man7.org/linux/man-pages/man5/sudoers.5.html">man 5 sudoers</a></li>
<li><a href="https://wiki.archlinux.org/title/Users_and_groups">Linux User Management Guide</a> (Arch Wiki, applies broadly)</li>
<li><a href="https://docs.freebsd.org/en/books/handbook/basics/#users-and-basic-account-management">FreeBSD Handbook: Users and Basic Account Management</a></li>
</ul>
<h3 id="macos-specific-resources">macOS-Specific Resources</h3>
<ul>
<li><a href="https://support.apple.com/guide/mac-help/add-a-user-or-group-mchl3e281fc9/mac">Add a user or group on Mac - Apple Support</a></li>
<li><a href="https://support.apple.com/guide/mac-help/change-users-groups-settings-mtusr001/mac">Change Users &amp; Groups settings on Mac - Apple Support</a></li>
<li><a href="https://ss64.com/mac/dscl.html">dscl Man Page - SS64.com</a> — comprehensive dscl reference</li>
<li><a href="https://www.macos.utah.edu/documentation/authentication/dscl.html">dscl Examples - University of Utah</a> — practical dscl usage</li>
<li><a href="https://developer.apple.com/library/archive/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/additionalfeatures.html">Additional Features - Apple Developer Archive</a> — includes user creation examples</li>
</ul>
<h3 id="history--lore">History &amp; Lore</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/User_identifier">Why is root UID 0?</a></li>
<li><a href="https://idolinux.com/nobody-user-linux-unix/">The nobody user</a></li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<p>Every file belongs to someone. Every process runs as someone.</p>
<p>Email me if you found this guide useful or if I missed something:<br>
<a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
<p>Happy administering</p>
]]></content:encoded>
    </item>
    <item>
      <title>Git Customization</title>
      <link>https://adminjitsu.com/posts/git-customization/</link>
      <pubDate>Thu, 18 Sep 2025 00:03:52 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/git-customization/</guid>
      <description>A practical guide to git config: persistence, safe defaults, aliases, and custom themes to make Git your own.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<blockquote>
<p><em>&ldquo;A rolling stone gathers momentum.&rdquo;</em> &ndash; anonymous fortune</p></blockquote>
<p>Git is one of those tools that grows with you. At a certain point I wondered what you could customize and found a number of quality of life improvements. You can tweak aliases, set safer defaults, and customize color schemes.</p>
<p>This guide is the companion piece to <strong><a href="/posts/git-fu/">Git-Fu</a></strong>. That one focused on commands and workflows. This one goes under the hood: <code>git config</code>, what lives in the config files, and how you can bend Git to your will.</p>
<h2 id="how-git-config-works">How <code>git config</code> Works</h2>
<p>Git&rsquo;s configuration is layered into three scopes:</p>
<ul>
<li><strong>System</strong>: applies to all users
<ul>
<li>Linux/Mac: <code>/etc/gitconfig</code></li>
<li>Windows: <code>C:\Program Files\Git\etc\gitconfig</code></li>
</ul>
</li>
<li><strong>Global</strong>: applies to you
<ul>
<li>Primary: <code>~/.gitconfig</code></li>
<li>Alternative: <code>~/.config/git/config</code> (XDG standard)</li>
</ul>
</li>
<li><strong>Local</strong>: applies to a single repo (<code>.git/config</code>)</li>
</ul>
<p>Git reads these in order, with more specific scopes overriding broader ones. So a local setting beats a global one, which beats a system one.</p>
<p>The safe way to set values is with the <code>git config</code> command, which writes to the correct file for you. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Your Name&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;you@example.com&#34;</span>
</span></span></code></pre></div><p>Use <code>--system</code>, <code>--global</code>, or <code>--local</code> to control the scope. Handy flags include <code>--get</code>, <code>--unset</code>, and <code>--list</code>.</p>
<p>See everything Git knows about you right now with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --list --show-origin
</span></span></code></pre></div><hr>
<h2 id="where-git-config-lives-persistence-saving--resetting">Where <code>git config</code> Lives (Persistence, Saving &amp; Resetting)</h2>
<p>When you run <code>git config</code>, you’re not changing runtime state — you’re editing a config file. That’s why these tweaks persist across reboots and repos.</p>
<h3 id="saving-your-config">Saving Your Config</h3>
<p>Back up everything:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --list &gt; my-gitconfig.txt
</span></span></code></pre></div><p>Or just a subset:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --get-regexp <span class="s1">&#39;^alias\.&#39;</span> &gt; my-aliases.txt
</span></span></code></pre></div><h3 id="restoring-a-config">Restoring a Config</h3>
<p>Reapply from a dump:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">while</span> <span class="nb">read</span> key value<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  git config --global <span class="s2">&#34;</span><span class="nv">$key</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$value</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> &lt; my-gitconfig.txt
</span></span></code></pre></div><p>Or just copy <code>~/.gitconfig</code> between machines.</p>
<h3 id="resetting-to-defaults">Resetting to Defaults</h3>
<p>Remove a section:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --remove-section <span class="nb">alias</span>
</span></span></code></pre></div><p>Unset a single key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --unset core.editor
</span></span></code></pre></div><p>Full reset:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mv ~/.gitconfig<span class="o">{</span>,.bak<span class="o">}</span>   <span class="c1"># quick backup</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># or, if you’re feeling reckless</span>
</span></span><span class="line"><span class="cl">rm ~/.gitconfig
</span></span></code></pre></div><p>⚠️ <span class="hl yellow">Nukes everything—identity, aliases, colors, etc.</span><br>
Use the <code>mv</code> method unless you’re 100% sure. Back up first!</p>
<h3 id="takeaways">Takeaways</h3>
<ul>
<li><code>git config</code> is <strong>persistent</strong> because it writes to config files.</li>
<li>You can <strong>save and restore</strong> with simple dump/reapply steps.</li>
<li>Resetting can be surgical (<code>--unset</code>) or nuclear (<code>rm ~/.gitconfig</code>).</li>
</ul>
<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ninja-slash.jpg" 
       alt="a video game style pixel art ninja slashing in a dramatic arc" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Ninjas make every post better!
  </figcaption>
</figure>
<h2 id="cheatsheet-useful-git-config-commands">Cheatsheet: Useful Git Config Commands</h2>
<p>The examples below use <code>git config</code> commands, but you can also open your config file directly with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --edit
</span></span></code></pre></div><hr>
<h3 id="quick-start-5-safe-defaults">Quick Start: 5 Safe Defaults</h3>
<p>If you only set a handful of Git configs, make it these. They improve safety, keep repos tidy, and save you headaches later:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Your Name&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;you@example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git config --global init.defaultBranch main   <span class="c1"># new repos start on &#39;main&#39;</span>
</span></span><span class="line"><span class="cl">git config --global push.default simple       <span class="c1"># safer pushes</span>
</span></span><span class="line"><span class="cl">git config --global fetch.prune <span class="nb">true</span>          <span class="c1"># auto-clean dead branches</span>
</span></span></code></pre></div><p>That’s it—your identity + three defaults. You can stop here and still have a nicer Git.<br>
When you’re ready, scroll down for more customization (aliases, pretty logs, colors, etc.).</p>
<hr>
<h3 id="identity">Identity</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;Your Name&#34;</span>
</span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;you@example.com&#34;</span>
</span></span></code></pre></div><hr>
<h3 id="usability-defaults">Usability Defaults</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global init.defaultBranch main   <span class="c1"># new repos start with &#39;main&#39;</span>
</span></span><span class="line"><span class="cl">git config --global push.default simple       <span class="c1"># safer pushes</span>
</span></span><span class="line"><span class="cl">git config --global fetch.prune <span class="nb">true</span>          <span class="c1"># auto-clean old branches</span>
</span></span><span class="line"><span class="cl">git config --global pull.rebase <span class="nb">false</span>         <span class="c1"># or true if you’re a rebasing fan</span>
</span></span></code></pre></div><hr>
<h3 id="editor">Editor</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global core.editor <span class="s2">&#34;nano&#34;</span>  <span class="c1"># or vim, code --wait, etc.</span>
</span></span><span class="line"><span class="cl">git config --global diff.tool vimdiff     <span class="c1"># or your preferred diff tool</span>
</span></span><span class="line"><span class="cl">git config --global merge.tool vimdiff    <span class="c1"># or your preferred merge tool</span>
</span></span></code></pre></div><hr>
<h3 id="pretty-logs">Pretty Logs</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global format.pretty oneline
</span></span><span class="line"><span class="cl">git config --global log.abbrevCommit <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git config --global log.date relative
</span></span></code></pre></div><hr>
<h3 id="safer-merges">Safer Merges</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global merge.ff only
</span></span><span class="line"><span class="cl">git config --global diff.mnemonicprefix <span class="nb">true</span>
</span></span><span class="line"><span class="cl">git config --global rerere.enabled <span class="nb">true</span>  <span class="c1"># reuse recorded resolutions</span>
</span></span></code></pre></div><hr>
<h3 id="aliases-muscle-memory-ftw">Aliases (muscle memory FTW)</h3>
<p>Aliases let you shorten Git commands into quick shorthands. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.st status
</span></span></code></pre></div><p>Now instead of typing <code>git status</code>, you can just type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git st
</span></span></code></pre></div><p>That’s all aliases do—they don’t add new features, but they save keystrokes and speed up common commands. Aliases can point to <strong>any Git verb</strong>, including ones with arguments. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.lg <span class="s2">&#34;log --oneline --decorate --graph --all&#34;</span>
</span></span><span class="line"><span class="cl">git lg   <span class="c1"># expands to &#39;git log --oneline --decorate --graph --all&#39;</span>
</span></span></code></pre></div><p>And here’s the cool part: if your alias starts with <code>!</code>, Git treats it as a raw <strong>shell command</strong> instead of a Git subcommand. That means you can make Git run non-Git commands—or Git commands wrapped in shell logic:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.today <span class="s1">&#39;!date&#39;</span>
</span></span><span class="line"><span class="cl">git today   <span class="c1"># runs your system &#39;date&#39; command</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git config --global alias.root <span class="s1">&#39;!git rev-parse --show-toplevel&#39;</span>
</span></span><span class="line"><span class="cl">git root    <span class="c1"># prints the repo’s root directory</span>
</span></span></code></pre></div><p><span class="tag yellow">Note:</span><br>
Aliases that start with <code>!</code> run exactly as written in your shell. They don’t take extra arguments the way normal Git commands do—<code>git root sub/dir</code> won’t work. For more flexibility, write a shell script and call it from your alias.</p>
<p>Here are some popular everyday aliases you can copy straight in:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global alias.st status
</span></span><span class="line"><span class="cl">git config --global alias.co checkout
</span></span><span class="line"><span class="cl">git config --global alias.br branch
</span></span><span class="line"><span class="cl">git config --global alias.cm <span class="s1">&#39;commit -m&#39;</span>
</span></span><span class="line"><span class="cl">git config --global alias.lg <span class="s2">&#34;log --oneline --decorate --graph --all&#34;</span>
</span></span></code></pre></div><br>
<h2 id="-how-git-colors-work">🎨 How Git Colors Work</h2>
<p>Git doesn’t store colors itself—it just spits out ANSI escape codes. Your <strong>terminal</strong> decides what “yellow” or “cyan” actually looks like. That’s why the default yellow sometimes looks like <em>Dijon mustard</em> depending on your theme.</p>
<p>Git supports three kinds of values:</p>
<ul>
<li><strong>Named ANSI colors</strong>: <code>red</code>, <code>green</code>, <code>blue</code>, <code>cyan</code>, <code>yellow</code>, <code>magenta</code>, <code>white</code>, <code>black</code></li>
<li><strong>Attributes</strong>: <code>bold</code>, <code>ul</code> (underline), <code>reverse</code>, <code>blink</code></li>
<li><strong>Hex RGB</strong> (<code>#rrggbb</code>) since Git 2.16 — the most reliable way to get <em>exact</em> shades across terminals</li>
</ul>
<hr>
<h3 id="quick-test">Quick Test</h3>
<p>If you just want to see Git output in color, enable it globally:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global color.ui always
</span></span></code></pre></div><p>Now run <code>git status</code> or <code>git diff</code> — you’ll get Git’s built-in red/green/yellow scheme.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Test your new colors</span>
</span></span><span class="line"><span class="cl">git status
</span></span><span class="line"><span class="cl">git log --oneline --decorate -10
</span></span><span class="line"><span class="cl">git diff HEAD~1 HEAD
</span></span><span class="line"><span class="cl">git branch -a
</span></span><span class="line"><span class="cl">git grep <span class="s2">&#34;function&#34;</span> <span class="c1"># (if you have any code files)</span>
</span></span></code></pre></div><hr>
<figure style="text-align:center; margin: 1em auto;">
  <img src="dj-ninja.jpg" 
       alt="a video game style pixel art ninja dj using a pair of turntables in a club" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<br>
<h3 id="sample-theme-cool-dark">Sample Theme: &ldquo;Cool Dark&rdquo;</h3>
<p>Here’s a complete theme you can paste in to try out. It uses <strong>golden yellow</strong>, <strong>light cyan/blue</strong>, <strong>mint green</strong>, and <strong>bright grey</strong> accents chosen for a black terminal background.</p>
<p>Paste the whole block into your terminal (or save as a script). VS Code&rsquo;s inline color picker (which appears when you hover over hex colors like <code>#ffd75f</code>) makes customizing these colors especially easy.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global color.ui always
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Branches ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git branch&#39; output and branch decorations in &#39;git log&#39;</span>
</span></span><span class="line"><span class="cl">git config --global color.branch.current <span class="s2">&#34;#ffd75f&#34;</span>   <span class="c1"># golden yellow (current branch)</span>
</span></span><span class="line"><span class="cl">git config --global color.branch.local   <span class="s2">&#34;#5fd7ff&#34;</span>   <span class="c1"># light cyan (local branches)</span>
</span></span><span class="line"><span class="cl">git config --global color.branch.remote  <span class="s2">&#34;#87afff&#34;</span>   <span class="c1"># soft sky blue (remotes)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Diffs ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git diff&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.meta       <span class="s2">&#34;#ffd75f&#34;</span>  <span class="c1"># metadata (index info, etc.)</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.frag       <span class="s2">&#34;#5fd7ff&#34;</span>  <span class="c1"># hunk headers</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.old        <span class="s2">&#34;#ff5f5f&#34;</span>  <span class="c1"># red for removed lines</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.new        <span class="s2">&#34;#87ffaf&#34;</span>  <span class="c1"># mint green for added lines</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.whitespace <span class="s2">&#34;#ff5f87&#34;</span>  <span class="c1"># pink highlight for whitespace errors</span>
</span></span><span class="line"><span class="cl">git config --global color.diff.commit     <span class="s2">&#34;#5fd7ff&#34;</span>  <span class="c1"># commit hashes in &#39;git log --oneline&#39; (older Git)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Status ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git status&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.status.added     <span class="s2">&#34;#87ffaf&#34;</span> <span class="c1"># mint green (staged new files)</span>
</span></span><span class="line"><span class="cl">git config --global color.status.changed   <span class="s2">&#34;#ffd75f&#34;</span> <span class="c1"># yellow (modified files)</span>
</span></span><span class="line"><span class="cl">git config --global color.status.untracked <span class="s2">&#34;#5fd7ff&#34;</span> <span class="c1"># cyan (new untracked files)</span>
</span></span><span class="line"><span class="cl">git config --global color.status.branch    <span class="s2">&#34;#bcbcbc&#34;</span> <span class="c1"># grey (branch info line)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Grep ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git grep&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.grep.match      <span class="s2">&#34;#ffd75f&#34;</span>  <span class="c1"># yellow (matches)</span>
</span></span><span class="line"><span class="cl">git config --global color.grep.filename   <span class="s2">&#34;#5fd7ff&#34;</span>  <span class="c1"># cyan (filenames)</span>
</span></span><span class="line"><span class="cl">git config --global color.grep.linenumber <span class="s2">&#34;#bcbcbc&#34;</span>  <span class="c1"># grey (line numbers)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Log / Decorations ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects &#39;git log --decorate&#39; output</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.head         <span class="s2">&#34;#87ffaf&#34;</span> <span class="c1"># mint green (HEAD)</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.branch       <span class="s2">&#34;#5fd7ff&#34;</span> <span class="c1"># cyan (local branches)</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.remoteBranch <span class="s2">&#34;#ffd75f&#34;</span> <span class="c1"># yellow (remote branches)</span>
</span></span><span class="line"><span class="cl">git config --global color.decorate.tag          <span class="s2">&#34;#ff5f87&#34;</span> <span class="c1"># pink (tags)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Interactive ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects interactive commands (git rebase -i, git add -p)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.prompt <span class="s2">&#34;#ffd75f&#34;</span> <span class="c1"># yellow (prompts)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.header <span class="s2">&#34;#87afff&#34;</span> <span class="c1"># blue (section headers)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.help   <span class="s2">&#34;#bcbcbc&#34;</span> <span class="c1"># grey (help text)</span>
</span></span><span class="line"><span class="cl">git config --global color.interactive.error  <span class="s2">&#34;#ff5f5f&#34;</span> <span class="c1"># red (errors)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ── Pager highlight ──</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Affects search matches inside &#39;less&#39;</span>
</span></span><span class="line"><span class="cl">git config --global color.pager.highlight <span class="s2">&#34;#87afff&#34;</span>   <span class="c1"># soft blue</span>
</span></span></code></pre></div><details >
  <summary>Details</summary>
  <div class="collapse-body">
    <p>This is what the exact same theme looks like if you edit your <code>~/.gitconfig</code> directly instead of running commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[color]</span>
</span></span><span class="line"><span class="cl">    <span class="na">ui</span> <span class="o">=</span> <span class="s">always</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;branch&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">current</span> <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    local   = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    remote  = #87afff</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;diff&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">meta</span>       <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    frag       = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    old        = #ff5f5f
</span></span></span><span class="line"><span class="cl"><span class="s">    new        = #87ffaf
</span></span></span><span class="line"><span class="cl"><span class="s">    whitespace = #ff5f87
</span></span></span><span class="line"><span class="cl"><span class="s">    commit     = #5fd7ff   # commit hashes (older Git)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;status&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">added</span>     <span class="o">=</span> <span class="s">#87ffaf
</span></span></span><span class="line"><span class="cl"><span class="s">    changed   = #ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    untracked = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    branch    = #bcbcbc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;grep&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">match</span>      <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    filename   = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    linenumber = #bcbcbc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;decorate&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">head</span>         <span class="o">=</span> <span class="s">#87ffaf
</span></span></span><span class="line"><span class="cl"><span class="s">    branch       = #5fd7ff
</span></span></span><span class="line"><span class="cl"><span class="s">    remoteBranch = #ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    tag          = #ff5f87</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;interactive&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">prompt</span> <span class="o">=</span> <span class="s">#ffd75f
</span></span></span><span class="line"><span class="cl"><span class="s">    header = #87afff
</span></span></span><span class="line"><span class="cl"><span class="s">    help   = #bcbcbc
</span></span></span><span class="line"><span class="cl"><span class="s">    error  = #ff5f5f</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[color &#34;pager&#34;]</span>
</span></span><span class="line"><span class="cl">    <span class="na">highlight</span> <span class="o">=</span> <span class="s">#87afff</span></span></span></code></pre></div>
  </div>
</details>

<figure style="text-align:center; margin: 1em auto;">
  <img src="git-cooldark.jpg" 
       alt="screenshot of git gcheck function output with cooldark theme applied" 
       style="display:block; margin:0 auto; width:min(100%, 8000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    It's nice to finally override the ugly default colors
  </figcaption>
</figure>
<hr>
<h3 id="resetting-to-defaults-1">Resetting to Defaults</h3>
<p>If you want Git’s classic red/green/yellow back, just clear your overrides:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global --remove-section color.branch   2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.diff     2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.status   2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.interactive 2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.grep     2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --remove-section color.pager    2&gt;/dev/null
</span></span><span class="line"><span class="cl">git config --global --unset color.ui                2&gt;/dev/null
</span></span></code></pre></div><br>
<h2 id="-further-reading">📚 Further Reading</h2>
<ul>
<li><a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration">Git Book: Customizing Git</a></li>
<li><a href="https://git-scm.com/docs/git-config">Git Documentation: <code>git-config</code></a></li>
<li><a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI color codes explained</a></li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="tsuba-2.png" 
       alt="a pixel art tsuba from a katana. decorative" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>If you’ve got tips, tricks, or a cool theme of your own, drop me a line: <a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Git-Fu</title>
      <link>https://adminjitsu.com/posts/git-fu/</link>
      <pubDate>Mon, 15 Sep 2025 04:09:11 -0500</pubDate>
      <guid>https://adminjitsu.com/posts/git-fu/</guid>
      <description>Git doesn’t have to feel cryptic. This guide breaks it down with clear metaphors, a structured cheatsheet, and real-world helpers like the `gcheck` function and handy aliases. From staging and commits to branching, merging, and undoing mistakes, learn how to navigate Git with confidence.</description>
      <content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>I&rsquo;ve been using git almost daily for years now and I can&rsquo;t imagine working on anything serious without it. I avoided learning git for ages though and just took my chances and used lots of backup folders and notes until my projects began to strain under their own weight.</p>
<p>Git can feel cryptic at first with numerous commands, it&rsquo;s own terminology, and plenty of <em>“oh no, what did I just do?”</em> moments. Luckily you don’t need to know every dark corner of Git to use it well. With a few key concepts, a solid cheatsheet, and some helper scripts, Git becomes a trusted and powerful tool.</p>
<h2 id="what-is-git">What is Git?</h2>
<p>At its heart, Git is a <strong>time machine for text files</strong>.<br>
It lets you move backward and forward through the history of a project, explore alternate timelines, and share those timelines with others.</p>
<ul>
<li>
<p><strong>Commits are snapshots.</strong><br>
Each commit is like a photograph of your project at a moment in time.<br>
It doesn’t just save what changed, but how the whole directory looked so you can always step back to that state.<br>
Specifically, Git stores full snapshots (not just diffs) and shows diffs only when you compare snapshots.</p>
</li>
<li>
<p><strong>Branches are pointers.</strong><br>
A branch isn’t a copy of your work — it’s just a movable label pointing to a commit.<br>
This makes branching lightweight and cheap, so you can spin off experiments freely without bloating your repo.<br>
The <code>HEAD</code> is a pointer too—it tells Git where you&rsquo;re currently standing.
Switching branches just moves <code>HEAD</code> (your current position) to a different branch reference.</p>
</li>
<li>
<p><strong>Merging is weaving timelines.</strong><br>
When two branches diverge, merging pulls their histories together into a single story.<br>
Sometimes this is automatic; other times you have to resolve conflicts where the timelines disagree.<br>
Sometimes Git can simply fast-forward a branch if there&rsquo;s no divergence—no new merge commit needed.</p>
</li>
<li>
<p><strong>Rebasing is replaying commits.</strong><br>
Instead of weaving two histories together, rebasing lifts your commits and replays them onto a different base.<br>
The result is a cleaner-looking history where events flow in a straight line.</p>
</li>
<li>
<p><strong>Remotes are copies of the archive.</strong><br>
A remote (like GitHub, GitLab, or Gitea) is just another place your repo lives.<br>
You can fetch from it, push to it, or clone it elsewhere. Remotes make collaboration and backup possible.</p>
</li>
</ul>
<p>Think of it as a <a href="https://en.wikipedia.org/wiki/Directed_graph">Directed graph</a> of changes where you always have a way back to safety</p>
<h2 id="key-concepts">Key Concepts</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="sensei.jpg" 
       alt="a video game style pixel art ninja and his master sitting in a dojo" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    Mastering Git just takes time and practice
  </figcaption>
</figure>
<ul>
<li>
<p><strong>Repos: local vs remote</strong><br>
A local repo is your working copy with a <code>.git/</code> folder inside it.<br>
A remote is just another full copy of the repo, usually hosted on a server (GitHub, GitLab, Gitea).<br>
By default, the first remote is called <code>origin</code>. You can rename it, or add others (<code>upstream</code>, <code>backup</code>, etc).</p>
</li>
<li>
<p><strong>Staging area</strong><br>
Also called the <em>index</em>. This is where you prepare changes before committing. Think of it as a shopping cart or draft tray: you can stage some edits, leave others unstaged, and commit only what you want.</p>
</li>
<li>
<p><strong>Commits</strong><br>
Immutable snapshots with parent pointers. Each has a SHA-1 ID (SHA-256 is becoming standard), author info, and message. Together they form a graph (specifically a <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">🔗directed acyclic graph or DAG</a> ) of your project history.</p>
</li>
<li>
<p><strong>Branches</strong><br>
Lightweight movable labels pointing at commits. Switching branches just moves the <code>HEAD</code> pointer to a different branch reference.</p>
</li>
<li>
<p><strong>Rebase vs Merge</strong></p>
<ul>
<li><em>Merge</em> combines two histories into one, preserving all bumps. Good for shared branches where others can see your work. Leaves a merge commi.</li>
<li><em>Rebase</em> rewrites your commits so they look like they happened on top of another branch, creating a cleaner chain of history.</li>
</ul>
</li>
<li>
<p><strong>Upstream/Tracking branches</strong><br>
When a local branch “tracks” a remote one, Git knows where to pull and push by default.</p>
</li>
</ul>
<h2 id="the-local-git-repo">The Local Git Repo</h2>
<p>When you <code>git init</code> in a folder, Git creates a hidden directory called <code>.git/</code>.<br>
That directory <em>is</em> the repository. All of your other files are just working copies.</p>
<p>This is what makes Git powerful and portable: if you copy a project folder with its <code>.git/</code> intact, you’ve copied the entire history as well as branches, tags, and settings. You don’t need a server, a database, or any special tooling.</p>
<h3 id="anatomy-of-git-simplified">Anatomy of <code>.git/</code> (simplified)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">└─$ tree -L <span class="m">1</span> .git/
</span></span><span class="line"><span class="cl">.git/
</span></span><span class="line"><span class="cl">├── COMMIT_EDITMSG
</span></span><span class="line"><span class="cl">├── FETCH_HEAD
</span></span><span class="line"><span class="cl">├── HEAD
</span></span><span class="line"><span class="cl">├── ORIG_HEAD
</span></span><span class="line"><span class="cl">├── branches
</span></span><span class="line"><span class="cl">├── config
</span></span><span class="line"><span class="cl">├── description
</span></span><span class="line"><span class="cl">├── hooks
</span></span><span class="line"><span class="cl">├── index
</span></span><span class="line"><span class="cl">├── info
</span></span><span class="line"><span class="cl">├── logs
</span></span><span class="line"><span class="cl">├── objects
</span></span><span class="line"><span class="cl">├── packed-refs
</span></span><span class="line"><span class="cl">└── refs
</span></span></code></pre></div><ul>
<li><strong>HEAD</strong> → a text file that tells Git which branch you’re currently on.</li>
<li><strong>config</strong> → local repo configuration (remotes, branch tracking, custom settings).</li>
<li><strong>objects/</strong> → the actual data store. Commits, trees, and blobs (file contents) live here, addressed by their SHA-1 hash.</li>
<li><strong>refs/</strong> → pointers to commits: branches, tags, remotes.</li>
<li><strong>logs/</strong> → the <em>reflog</em>, Git’s “black box recorder” of branch movements.</li>
<li><strong>index</strong> → the staging area, stored as a binary file.</li>
<li><strong>hooks/</strong> → scripts you can run automatically on events (commits, pushes, merges).</li>
</ul>
<p>Most of the time you never touch these directly, but it’s useful to know they exist.<br>
Thankfully it’s just a folder full of plain-text files and hashed objects. (Peek at <code>COMMIT_EDITMSG</code> if you ever want to see the last commit message you wrote.)</p>
<h3 id="why-it-matters">Why it matters</h3>
<ul>
<li><strong>Portability</strong> → copy the folder to a USB stick, another machine, or zip it up, and you’ve moved the repo with its entire history.</li>
<li><strong>Independence</strong> → no central server required; you always have a complete copy locally.</li>
<li><strong>Transparency</strong> → advanced users can inspect <code>.git/config</code> or <code>.git/refs/heads/</code> to see what’s really happening under the hood.</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="commit-ninja.jpg" 
       alt="a video game style pixel art ninja holding a scroll bearing the word Commit" 
       style="display:block; margin:0 auto; width:min(100%, 400px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<br>
<h2 id="cheatsheet">Cheatsheet</h2>
<p>Git has a lot of commands so I like to group them into related tasks to make it easier to find what you need based on what you&rsquo;re trying to do.</p>
<h3 id="getting-started-init--remotes"><span style="color:#CC0000;">Getting Started: Init &amp; Remotes</span></h3>
<p>Every Git journey starts by either <strong>creating a new repo</strong> or <strong>cloning an existing one</strong>.<br>
Then, if you want to sync with a remote (GitHub, GitLab, Gitea), you connect it.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Create a new repo ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Initialize a repo in the current folder</span>
</span></span><span class="line"><span class="cl">git init
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clone an existing repo</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/user/project.git
</span></span><span class="line"><span class="cl">git clone git@github.com:user/project.git   <span class="c1"># SSH form</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Check current remotes ===</span>
</span></span><span class="line"><span class="cl">git remote -v
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a new remote (usually &#39;origin&#39;)</span>
</span></span><span class="line"><span class="cl">git remote add origin https://github.com/user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change the remote URL (e.g., switch to SSH)</span>
</span></span><span class="line"><span class="cl">git remote set-url origin git@github.com:user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a remote</span>
</span></span><span class="line"><span class="cl">git remote remove origin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === First push ===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Push the current branch to &#39;origin&#39; and set it as upstream</span>
</span></span><span class="line"><span class="cl">git push -u origin main
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># After upstream is set, future pushes are simpler:</span>
</span></span><span class="line"><span class="cl">git push
</span></span><span class="line"><span class="cl">git pull</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>git init</code> creates a <code>.git/</code> folder; that’s the repo.</li>
<li>HTTPS is simpler to start with, but SSH keys are better long-term (no passwords every push).</li>
<li>The <code>-u</code> flag on first push links your branch to the remote, so you can just <code>git push</code> / <code>git pull</code> afterward.</li>
<li>You can have multiple remotes (e.g., <code>origin</code> for GitHub, <code>backup</code> for Gitea).</li>
</ul>
  </div>
</details>

<hr>
<h3 id="daily-workflow-add-commit-push-pull"><span style="color:#CC0000;">Daily Workflow: Add, Commit, Push, Pull</span></h3>
<p>Most of the time you’re just making changes, saving them, and syncing with a remote.<br>
These are the <strong>everyday Git commands</strong> you’ll run dozens of times a week.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Checking status ===</span>
</span></span><span class="line"><span class="cl">git status          <span class="c1"># See which files changed, staged, or untracked</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Staging changes ===</span>
</span></span><span class="line"><span class="cl">git add file.txt    <span class="c1"># Stage one file</span>
</span></span><span class="line"><span class="cl">git add -A          <span class="c1"># Stage all changes (tracked + untracked)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Committing ===</span>
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Message here&#34;</span>   <span class="c1"># Save staged changes with a message</span>
</span></span><span class="line"><span class="cl">git commit --amend             <span class="c1"># Fix last commit (message or staged content)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pushing to remote ===</span>
</span></span><span class="line"><span class="cl">git push            <span class="c1"># Send local commits to upstream</span>
</span></span><span class="line"><span class="cl">git push origin main  <span class="c1"># Explicit remote + branch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pulling updates ===</span>
</span></span><span class="line"><span class="cl">git pull            <span class="c1"># Fetch + merge changes from upstream</span>
</span></span><span class="line"><span class="cl">git pull --rebase   <span class="c1"># Fetch + replay your commits on top (cleaner history)</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>git status</code> is your compass — run it constantly.</li>
<li><code>git add</code> = “move to staging area” → nothing is committed until you do.</li>
<li><code>git commit --amend</code> is safe <em>if you haven’t pushed yet</em>.</li>
<li>The first <code>git push</code> usually needs <code>-u origin main</code> to set upstream (covered in <em>Init &amp; Remotes</em>).</li>
<li>Prefer <code>git pull --rebase</code> to avoid “merge commits” from trivial updates.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="branching--merging"><span style="color:#CC0000;">Branching &amp; Merging</span></h3>
<p>Branches let you explore ideas without messing up your main line of work.<br>
Merging and rebasing are how you bring those timelines back together.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Working with branches ===</span>
</span></span><span class="line"><span class="cl">git branch               <span class="c1"># List local branches</span>
</span></span><span class="line"><span class="cl">git branch -r            <span class="c1"># List remote branches</span>
</span></span><span class="line"><span class="cl">git branch new-feature   <span class="c1"># Create a new branch (stays on current)</span>
</span></span><span class="line"><span class="cl">git checkout new-feature <span class="c1"># Switch to a branch (old syntax)</span>
</span></span><span class="line"><span class="cl">git switch new-feature   <span class="c1"># Switch to a branch (new syntax)</span>
</span></span><span class="line"><span class="cl">git switch -c bugfix     <span class="c1"># Create + switch in one step</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Merging ===</span>
</span></span><span class="line"><span class="cl">git checkout main
</span></span><span class="line"><span class="cl">git merge new-feature    <span class="c1"># Merge branch into main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Merge conflicts: Git marks conflicts in files</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Edit manually, then:</span>
</span></span><span class="line"><span class="cl">git add conflicted-file
</span></span><span class="line"><span class="cl">git commit               <span class="c1"># Finish the merge</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Rebasing ===</span>
</span></span><span class="line"><span class="cl">git checkout new-feature
</span></span><span class="line"><span class="cl">git rebase main          <span class="c1"># Replay commits on top of main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># If conflicts happen during rebase:</span>
</span></span><span class="line"><span class="cl">git status               <span class="c1"># See what’s wrong</span>
</span></span><span class="line"><span class="cl">git add fixed-file
</span></span><span class="line"><span class="cl">git rebase --continue    <span class="c1"># Resume rebase</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Abort if it goes sideways</span>
</span></span><span class="line"><span class="cl">git merge --abort        <span class="c1"># during a merge</span>
</span></span><span class="line"><span class="cl">git rebase --abort       <span class="c1"># during a rebase</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Use <code>git branch -a</code> to see local + remote branches together.</li>
<li><code>git switch</code> is the modern command; <code>git checkout</code> still works.</li>
<li>Merges preserve full history (good for collaboration).</li>
<li>Rebases rewrite history (good for keeping it clean, but don’t rebase shared branches).</li>
<li>If things go wrong, <code>git status</code> and <code>git reflog</code> are your lifelines.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="undo--rescue"><span style="color:#CC0000;">Undo &amp; Rescue</span></h3>
<p>Everyone makes mistakes in Git from time to time. Luckily,<br>
Git has a <strong>time machine and a safety net</strong> built in. These commands let you undo changes, roll back commits, and recover “lost” history.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Unstaging changes ===</span>
</span></span><span class="line"><span class="cl">git restore --staged file.txt    <span class="c1"># Unstage a file (leave working copy alone)</span>
</span></span><span class="line"><span class="cl">git reset HEAD file.txt          <span class="c1"># Older syntax, same effect</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Discarding local changes ===</span>
</span></span><span class="line"><span class="cl">git restore file.txt             <span class="c1"># Throw away unstaged changes</span>
</span></span><span class="line"><span class="cl">git checkout -- file.txt         <span class="c1"># Old form, still works</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Undoing commits ===</span>
</span></span><span class="line"><span class="cl">git commit --amend               <span class="c1"># Edit last commit (message or staged content)</span>
</span></span><span class="line"><span class="cl">git reset --soft HEAD~1          <span class="c1"># Undo last commit, keep changes staged</span>
</span></span><span class="line"><span class="cl">git reset --mixed HEAD~1         <span class="c1"># Undo last commit, keep changes unstaged</span>
</span></span><span class="line"><span class="cl">git reset --hard HEAD~1          <span class="c1"># Rewind + throw away changes (⚠️ destructive)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Reverting commits safely ===</span>
</span></span><span class="line"><span class="cl">git revert &lt;commit&gt;              <span class="c1"># Create a new commit that undoes the given one</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Reflog (magic undo history) ===</span>
</span></span><span class="line"><span class="cl">git reflog                       <span class="c1"># Show where HEAD has been (local movements)</span>
</span></span><span class="line"><span class="cl">git checkout &lt;commit-hash&gt;       <span class="c1"># Recover a commit from reflog</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Aborting operations ===</span>
</span></span><span class="line"><span class="cl">git merge --abort                <span class="c1"># Cancel a merge in progress</span>
</span></span><span class="line"><span class="cl">git rebase --abort               <span class="c1"># Cancel a rebase in progress</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Use <code>git restore</code> for day-to-day “oops” moments (unstaging or discarding edits).</li>
<li><code>git reset</code> is more powerful — soft/mixed/hard decide what happens to your changes.</li>
<li><code>git revert</code> is safer in shared branches because it adds a new commit instead of rewriting history.</li>
<li><code>git reflog</code> is the secret weapon — even after resets or rebases, you can usually recover.</li>
<li>When in doubt: stop, run <code>git status</code>, then check the reflog before panicking.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="remotes--sync"><span style="color:#CC0000;">Remotes &amp; Sync</span></h3>
<p>Remotes are just other copies of your repo (GitHub, GitLab, Gitea, etc.).<br>
You fetch changes from them, push your commits to them, and pull to stay in sync.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Checking remotes ===</span>
</span></span><span class="line"><span class="cl">git remote -v                   <span class="c1"># Show remotes and their URLs</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add a remote (commonly &#39;origin&#39;)</span>
</span></span><span class="line"><span class="cl">git remote add origin https://github.com/user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change URL (switch HTTPS ↔ SSH)</span>
</span></span><span class="line"><span class="cl">git remote set-url origin git@github.com:user/project.git
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Fetching &amp; pulling ===</span>
</span></span><span class="line"><span class="cl">git fetch                       <span class="c1"># Download commits/branches from remote (no merge)</span>
</span></span><span class="line"><span class="cl">git pull                        <span class="c1"># Fetch + merge remote changes into current branch</span>
</span></span><span class="line"><span class="cl">git pull --rebase               <span class="c1"># Fetch + replay your commits on top (cleaner history)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pushing ===</span>
</span></span><span class="line"><span class="cl">git push                        <span class="c1"># Push current branch to its upstream</span>
</span></span><span class="line"><span class="cl">git push -u origin main         <span class="c1"># First push: set upstream branch</span>
</span></span><span class="line"><span class="cl">git push origin feature         <span class="c1"># Push a different branch by name</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Push all tags</span>
</span></span><span class="line"><span class="cl">git push --tags
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Delete a remote branch</span>
</span></span><span class="line"><span class="cl">git push origin --delete old-branch</span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li><code>git fetch</code> is safe — it just updates your local view of the remote without touching your files.</li>
<li>Use <code>git pull --rebase</code> to keep history clean (avoids “merge commit” clutter).</li>
<li>The <code>-u</code> flag sets tracking so you can just run <code>git push</code> / <code>git pull</code> afterward.</li>
<li>Remotes are just names: you can have multiple (<code>origin</code>, <code>backup</code>, <code>upstream</code>).</li>
</ul>
  </div>
</details>

<hr>
<h3 id="tags--releases"><span style="color:#CC0000;">Tags &amp; Releases</span></h3>
<p>Tags mark important points in history — often used for releases (<code>v1.0</code>, <code>v2.1.3</code>).<br>
Unlike branches, they don’t move: they’re permanent labels on commits.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Creating tags ===</span>
</span></span><span class="line"><span class="cl">git tag v1.0.0                 <span class="c1"># Lightweight tag on latest commit</span>
</span></span><span class="line"><span class="cl">git tag -a v1.0.0 -m <span class="s2">&#34;Release&#34;</span> <span class="c1"># Annotated tag (with message, recommended)</span>
</span></span><span class="line"><span class="cl">git tag v1.0.0 &lt;commit&gt;        <span class="c1"># Tag a specific commit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Listing &amp; inspecting ===</span>
</span></span><span class="line"><span class="cl">git tag                        <span class="c1"># List all tags</span>
</span></span><span class="line"><span class="cl">git show v1.0.0                <span class="c1"># Show commit + info behind a tag</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pushing tags ===</span>
</span></span><span class="line"><span class="cl">git push origin v1.0.0         <span class="c1"># Push one tag</span>
</span></span><span class="line"><span class="cl">git push --tags                <span class="c1"># Push all tags</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Deleting tags ===</span>
</span></span><span class="line"><span class="cl">git tag -d v1.0.0              <span class="c1"># Delete local tag</span>
</span></span><span class="line"><span class="cl">git push origin --delete v1.0.0 <span class="c1"># Delete remote tag</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Prefer annotated tags (<code>-a</code>) — they carry messages and metadata.</li>
<li>Tags are great for marking release points or milestones.</li>
<li>GitHub/GitLab can auto-generate releases when you push a tag.</li>
</ul>
  </div>
</details>

<hr>
<h3 id="cleanup--maintenance"><span style="color:#CC0000;">Cleanup &amp; Maintenance</span></h3>
<p>Over time, branches pile up, and Git’s object database grows.<br>
These commands keep your repo tidy and lean.</p>
<details >
  <summary>Details</summary>
  <div class="collapse-body">
    <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># === Branch cleanup ===</span>
</span></span><span class="line"><span class="cl">git branch -d feature          <span class="c1"># Delete local branch (safe, only if merged)</span>
</span></span><span class="line"><span class="cl">git branch -D feature          <span class="c1"># Force delete local branch</span>
</span></span><span class="line"><span class="cl">git push origin --delete old   <span class="c1"># Delete remote branch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Pruning ===</span>
</span></span><span class="line"><span class="cl">git fetch --prune              <span class="c1"># Remove local refs to deleted remote branches</span>
</span></span><span class="line"><span class="cl">git remote prune origin        <span class="c1"># Same effect, manual trigger</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Garbage collection ===</span>
</span></span><span class="line"><span class="cl">git gc                         <span class="c1"># Cleanup loose objects &amp; optimize repo</span>
</span></span><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now             <span class="c1"># Aggressive cleanup (⚠️ careful)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># === Checking size ===</span>
</span></span><span class="line"><span class="cl">git count-objects -vH          <span class="c1"># Show object counts + repo size</span></span></span></code></pre></div>  
<p><strong>Notes:</strong></p>
<ul>
<li>Always check <code>git branch -a</code> before deleting — make sure the branch isn’t needed.</li>
<li><code>git fetch --prune</code> is safe to run regularly (it just cleans stale refs).</li>
<li><code>git gc</code> runs automatically sometimes, but you can run it manually if a repo feels sluggish.</li>
<li>For giant repos, consider tools like <a href="https://rtyley.github.io/bfg-repo-cleaner/">BFG Repo-Cleaner</a> to remove large files from history.</li>
</ul>
  </div>
</details>

<br>
<figure style="text-align:center; margin: 1em auto;">
  <img src="bamboo.jpg" 
       alt="a pixel art ninja dashing through a bamboo forest in a dramatic action pose" 
       style="display:block; margin:0 auto; width:min(100%, 500px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<h2 id="tricky-areas-demystified">Tricky Areas Demystified</h2>
<p>There are a few tricky situations that hopefully save a lot of head-scratching.</p>
<h3 id="merge-vs-rebase">Merge vs Rebase</h3>
<p>Both are ways of bringing one branch’s changes into another, but they tell different stories:</p>
<ul>
<li>
<p><strong>Merge</strong> → preserves history. Two timelines come together, and the graph shows the divergence + merge point.</p>
<ul>
<li>Good for shared branches (everyone sees the real history).</li>
<li>Downside: history can look “messy” with lots of merge commits.</li>
</ul>
</li>
<li>
<p><strong>Rebase</strong> → rewrites history. Your commits are replayed as if they happened after the other branch.</p>
<ul>
<li>Good for private branches you haven’t shared yet (keeps history linear).</li>
<li>Downside: <span class="hl orange">don’t rebase</span> commits you’ve already pushed/shared — it confuses collaborators.</li>
</ul>
</li>
</ul>
<p>💡 <em>Rule of thumb:</em> Merge when collaborating, rebase when cleaning up your own local work.</p>
<hr>
<h3 id="how-git-diff-works">How Git Diff Works</h3>
<p>Diffs compare <strong>snapshots</strong>, not files directly. That’s why you can run diffs between:</p>
<ul>
<li><strong>Working directory vs staging</strong> → <code>git diff</code></li>
<li><strong>Staging vs last commit</strong> → <code>git diff --cached</code></li>
<li><strong>Any two commits</strong> → <code>git diff &lt;commit1&gt; &lt;commit2&gt;</code></li>
<li><strong>Current branch vs remote</strong> → <code>git diff origin/main</code></li>
</ul>
<p>This flexibility comes from Git’s DAG of commits. A “diff” is just “what would I need to apply to one snapshot to make it look like another?”</p>
<hr>
<h3 id="detached-head">Detached HEAD</h3>
<p>When you check out a commit by hash (not a branch), <code>HEAD</code> points directly at that commit.<br>
You can poke around safely, but new commits won’t belong to any branch unless you explicitly create one.</p>
<ul>
<li>Checking out a commit: <code>git checkout abc123</code></li>
<li>Getting back to a branch: <code>git switch main</code></li>
<li>Saving your detached work: <code>git switch -c experiment</code></li>
</ul>
<hr>
<h3 id="the-safety-net-reflog">The Safety Net: Reflog</h3>
<p>Every time <code>HEAD</code> moves, Git logs it in <code>.git/logs/HEAD</code>.<br>
That means even after a reset, rebase, or branch delete, you can usually recover.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git reflog              <span class="c1"># Show recent HEAD movements</span>
</span></span><span class="line"><span class="cl">git checkout &lt;hash&gt;     <span class="c1"># Jump back to a lost commit</span>
</span></span></code></pre></div><p>Think of it as Git’s <strong>black box recorder</strong>. When all else fails, check the reflog.</p>
<br>
<h2 id="gcheck-a-friendly-git-status-report">gcheck: A Friendly Git Status Report</h2>
<p>Over time I found myself running the same few Git commands again and again:</p>
<ul>
<li><code>git status</code> to see what’s staged</li>
<li><code>git fetch</code> to check for updates</li>
<li><code>git log</code> or <code>git diff</code> to compare with upstream</li>
</ul>
<p>That’s a lot of typing just to answer the question: <em>“What’s the state of this repo?”</em></p>
<p>So I wrote a little Bash function called <code>gcheck</code>. It gives you a clear, human-readable summary of your current repository, including whether you’re ahead or behind your remote branch. I’ve been using it daily for months now and it’s become second nature.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcheck<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! git rev-parse --show-toplevel <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;🚫 Not inside a Git repository.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🔍 Checking Git repo status for: </span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="k">$(</span>git rev-parse --show-toplevel<span class="k">)</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;------------------------------------------&#34;</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  git status <span class="o">||</span> <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;\n📡 Fetching updates from remote...&#34;</span>
</span></span><span class="line"><span class="cl">  git fetch <span class="o">||</span> <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> branch current remote
</span></span><span class="line"><span class="cl">  <span class="nv">current</span><span class="o">=</span><span class="k">$(</span>git symbolic-ref --short HEAD<span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">remote</span><span class="o">=</span><span class="k">$(</span>git <span class="k">for</span>-each-ref --format<span class="o">=</span><span class="s1">&#39;%(upstream:short)&#39;</span> <span class="s2">&#34;refs/heads/</span><span class="nv">$current</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> -z <span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;⚠️  No remote tracking branch set for &#39;</span><span class="nv">$current</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;\n🔄 Comparing local &#39;</span><span class="nv">$current</span><span class="s2">&#39; with &#39;</span><span class="nv">$remote</span><span class="s2">&#39;...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> ahead behind
</span></span><span class="line"><span class="cl">  <span class="nv">ahead</span><span class="o">=</span><span class="k">$(</span>git rev-list --count <span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span>..HEAD<span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">behind</span><span class="o">=</span><span class="k">$(</span>git rev-list --count HEAD..<span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">((</span> <span class="nv">ahead</span> <span class="o">==</span> <span class="m">0</span> <span class="o">&amp;&amp;</span> <span class="nv">behind</span> <span class="o">==</span> <span class="m">0</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;✅ Branch is up to date with &#39;</span><span class="nv">$remote</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;⚠️  Local is </span><span class="nv">$ahead</span><span class="s2"> commit(s) ahead and </span><span class="nv">$behind</span><span class="s2"> commit(s) behind &#39;</span><span class="nv">$remote</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -e <span class="s2">&#34;\n📋 Commits on remote not in local:&#34;</span>
</span></span><span class="line"><span class="cl">    git log HEAD..<span class="s2">&#34;</span><span class="nv">$remote</span><span class="s2">&#34;</span> --oneline
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -e <span class="s2">&#34;\n🔍 Run this to view changes:\n  git diff </span><span class="nv">$remote</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>I drop this in my shell config (<code>~/.bashrc</code> / <code>~/.zshrc</code>) and now <code>gcheck</code> is just part of my daily workflow.</p>
<figure style="text-align:center; margin: 1em auto;">
  <img src="gcheck-issues.jpg" 
       alt="output of the gcheck function showing some files and sync issues" 
       style="display:block; margin:0 auto; width:min(100%, 900px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    gcheck combines several commands in a nice simple format to show you the status of your repo at a glance
  </figcaption>
</figure>
<figure style="text-align:center; margin: 1em auto;">
  <img src="gcheck-clean.jpg" 
       alt="output of the gcheck function showing a clean, synced repo" 
       style="display:block; margin:0 auto; width:min(100%, 800px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    This display showing a cleanly synced repo is always a welcome sight
  </figcaption>
</figure>
<h3 id="why-i-like-it">Why I like it</h3>
<ul>
<li>One glance tells you if you’re up to date with upstream.</li>
<li>If you’re behind, it lists the missing commits so you can see what’s coming.</li>
<li>If you’re ahead, it reminds you to push.</li>
<li>If you’re both, then you know it’s merge/rebase time.</li>
<li>And if you’re not even in a Git repo, it politely tells you. I do that all the time so it&rsquo;s much appreciated.</li>
</ul>
<figure style="text-align:center; margin: 1em auto;">
  <img src="ninja-meditate.jpg" 
       alt="a video game style pixel art ninja seated in a meditation pose surrounded by swirling blue energy and symbols" 
       style="display:block; margin:0 auto; width:min(100%, 600px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<h2 id="handy-aliases">Handy Aliases</h2>
<p>I use the following aliases in my environments. <code>glo</code> is especially useful but long and hard to remember. The others are nice when you find yourself running the full command for the thousandth time while working on a project. You can simply add the following to your shell startup files (<code>.bashrc</code> or <code>.zshrc</code>) and reload.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Git</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gs</span><span class="o">=</span><span class="s1">&#39;git status&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">glo</span><span class="o">=</span><span class="s1">&#39;git log --oneline --graph --decorate --all&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">ga</span><span class="o">=</span><span class="s1">&#39;git add&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gc</span><span class="o">=</span><span class="s1">&#39;git commit&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gd</span><span class="o">=</span><span class="s1">&#39;git diff&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gco</span><span class="o">=</span><span class="s1">&#39;git checkout&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gp</span><span class="o">=</span><span class="s1">&#39;git push&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gl</span><span class="o">=</span><span class="s1">&#39;git pull&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">alias</span> <span class="nv">gitcha</span><span class="o">=</span><span class="s1">&#39;gcheck&#39;</span> <span class="c1"># an alias i&#39;m used to for the gcheck function</span>
</span></span></code></pre></div><figure style="text-align:center; margin: 1em auto;">
  <img src="git-glo.jpg" 
       alt="output of glo alias, git log --oneline --graph --decorate --all" 
       style="display:block; margin:0 auto; width:min(100%, 1000px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
    <code>glo</code> is a really handy alias that I run all the time
  </figcaption>
</figure>
<br>
<h2 id="links--stuff">Links &amp; Stuff</h2>
<h3 id="core-learning-resources"><strong>Core Learning Resources</strong></h3>
<ul>
<li><a href="https://git-scm.com/book/en/v2">Pro Git book (free online)</a> — The definitive Git reference</li>
<li><a href="https://www.atlassian.com/git/tutorials">Atlassian Git Tutorials</a> — Visual, task-oriented guides</li>
<li><a href="https://learngitbranching.js.org/">Learn Git Branching</a> — Interactive visual tutorial</li>
<li><a href="https://gitimmersion.com/">Git Immersion</a> — Hands-on walkthrough tutorial</li>
<li><a href="https://en.wikipedia.org/wiki/Git">Git (Wikipedia)</a> — History, design, and technical details</li>
</ul>
<h3 id="visual--interactive-tools"><strong>Visual &amp; Interactive Tools</strong></h3>
<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph">Git Graph (VS Code extension)</a> — Visual repo browser</li>
<li><a href="https://www.gitkraken.com/">GitKraken</a> — Popular GUI client</li>
<li><a href="https://www.sourcetreeapp.com/">Sourcetree</a> — Free visual Git client</li>
</ul>
<h3 id="advancedspecialized"><strong>Advanced/Specialized</strong></h3>
<ul>
<li><a href="https://ohshitgit.com/">Oh Shit, Git!?!</a> — How to fix common Git mistakes</li>
<li><a href="http://www-cs-students.stanford.edu/~blynn/gitmagic/">Git Magic</a> — Advanced concepts explained simply</li>
<li><a href="https://github.com/jupyter/nbdime">nbdime</a> — Better jupyter notebook diffs for data science</li>
<li><a href="https://pre-commit.com/">pre-commit</a> — Git hooks framework for code quality</li>
</ul>
<h3 id="quick-reference"><strong>Quick Reference</strong></h3>
<ul>
<li><a href="https://training.github.com/downloads/github-git-cheat-sheet.pdf">Git Cheat Sheet (GitHub)</a> — Official PDF cheat sheet</li>
</ul>
<br>
<h2 id="conclusion">Conclusion</h2>
<figure style="text-align:center; margin: 1em auto;">
  <img src="tsuba.png" 
       alt="a pixel art tsuba or guard from a katana" 
       style="display:block; margin:0 auto; width:min(100%, 300px); height:auto;">
  <figcaption style="font-size: 85%; font-weight: normal; color: #666; line-height:1.4; margin-top:0.4em;">
  </figcaption>
</figure>
<p>Git is a deep and sometimes intimidating system, but it is a really powerful part of a project or workflow.</p>
<p>With just a handful of concepts, a few daily commands, and some helpers like <code>gcheck</code>, you can navigate most workflows with confidence.</p>
<p><a href="mailto:feedback@adminjitsu.com">feedback@adminjitsu.com</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
