← Home

Updated September 2026

Tools, Workflows, and My Tech Stack

How I actually work: the tools on my machine, the workflows I lean on, and the technologies I reach for when building and operating systems. I run most of this as a single operator, so it is all biased toward reproducibility, automation, and keeping the blast radius small.

Tools

Editor

. Modal, no plugin system, LSP and tree-sitter built in. The theme picker on this site is generated from every one of its bundled themes.

In Terminal

I write most of my code in a terminal.

Yes. I know this is crazy. The thing is, terminal editors have gotten very good, plus most of my editing happens over SSH, not locally. Yes, there are ways to run VS Code over SSH connections, but a lot of the times I might need to only make one or two small edits here and there - not enough to spawn a VS Code server for a client to connect to.

I didn’t always feel this way. I too had trouble exiting vim the first time I tried it. nvim let me customize the experience, but still didn’t make me feel productive.

Kakoune and its subject-then-action was a revelation for me in this space. Once I discovered it, after some practice, I realized I could write code much faster than I could in a more graphical tool like VS Code.

But still, it didn’t feel quite there.

Helix specifically is what turned me into a believer. It has the productive subject-then-action mechanics that Kakoune has (over vim’s action-then-subject), and has everything I need, generally, out of the box.

The only thing it really doesn’t have is integration with a debugger (though linters and language-servers can all be connected). These days though, thanks to things like pdb++, that really isn’t a big issue.

But how can you…

Read more than one file at once?

I can have more than one tab (buffer) open. Helix with multiple buffers open at once

Or, I can split my terminal into tiles! Helix with multiple windows open at once

Search a whole project?

I can search for files by name Helix file name search

I can search file contents for a regex string Helix file content search

VS Code

But not always.

Helix covers almost everything I need, generally, out of the box. A few things still send me back to a GUI editor, and I’d rather admit that than pretend otherwise.

The biggest one is debugging. pdb++ covers Python well enough that it barely counts as a gap for the terminal workflow. It doesn’t cover everything else. When I’m chasing a bug in a language or runtime where the debugger story is thinner, or the bug only really gives itself up with real breakpoints, watch expressions, and stepping through a call stack visually, VS Code’s debugger is just better tooling for that specific job. Pretending otherwise would slow me down for no good reason.

The other one is other people. If I’m pairing with someone, or walking a client through a change over a screen share, asking them to also pick up subject-then-action modal editing first is a bad use of everyone’s time. VS Code is the thing almost anyone can already read, so that’s what goes on screen.

And sometimes it really is just the ecosystem: a language, framework, or notebook-style workflow where the good tooling happens to live in a VS Code extension and nowhere else yet.

None of that changes the default. Most editing still happens in Helix, over SSH, on whatever box the work actually lives on. VS Code comes out for the specific situations above, not because the terminal stopped being enough.

Runtime & tasks

to pin language and tool versions per project and to drive per-repo task runners and environment.

From GNU Make to mise-en-place

From Make to mise-en-place.

For years, Make was just what I reached for. It’s already installed everywhere, make build / make test / make deploy targets are a convention most people already know, and PHONY targets are good enough as a task runner for most things.

What Make never did was touch tool versions. A Makefile could run node build.js, but it had no opinion about which node. That lived somewhere else entirely: nvm for Node, pyenv for Python, rbenv for Ruby, each pinned by its own dotfile, in its own format, read by its own shell hook. The Makefile and the version manager never actually talked to each other. It worked, right up until it didn’t, a fresh clone with the wrong global version quietly running the wrong thing before anyone noticed.

mise is what finally closed that gap, and the name is the whole pitch: mise en place, everything in its place before you start cooking. One file pins the tool versions a project needs and defines the tasks that build, run, and test it, so the two things Make and a version manager used to handle separately, and inconsistently, live in the same place and get read the same way.

This site is the plainest example: mise.toml pins Node, fnox, age, and hadolint, and mise run build, mise run start, and mise run test:e2e are the entire interface to building, running, and testing it. A fresh checkout, on any machine, gets the right versions automatically instead of “works on my machine.” Bigger infrastructure gets exactly the same treatment, not a special case just because the project is larger.

I still respect Make for what it actually is. I just don’t reach for it as a task runner anymore, now that there’s a tool that also handles the half of the problem Make was never built to solve.

Mise Cookbook

Mise Cookbook

My baseline mise.toml starting point for each language, one snippet per language below. Copy, drop into a fresh project, adjust the pinned version.

Python

My baseline for a python environment in mise-en-place

[tools]
# Statically define the python version.
python = "3.13"
# The language-server configured for Python in my Helix Editor config.
ruff = "latest"

[env]
# Where to create the python virtual environment
_.python.venv = {path = ".venv", create = true }

# Message of the Day for the project
# This hook runs when the project is entered. Changing directories within the
# project does not trigger it again.
[hooks.enter]
run = '''
cat << HEREDOC
##########################################################
 Welcome to {{ config_root | basename }}
##########################################################
 To get started, ...
HEREDOC
'''

Containers

, rootless and daemonless.

Rootless Podman over Rootfull Docker

Rootless, or it doesn’t run.

Docker’s daemon runs as a single, typically root owned, background process. Every container command goes through it, and anything that can reach that socket can effectively act as root on the host. I spent years accepting that as just how containers worked.

Podman doesn’t have a daemon at all. Containers run as regular child processes of whichever user started them, rootless by default, with no privileged service sitting there the whole time waiting to be the thing that gets compromised. On projects that also carry NIST 800-171 / CMMC 2.0 obligations, that’s not a nice to have, it’s one less thing to explain in an audit and one less way for a container escape to turn into a host compromise.

Switching didn’t cost much. Podman’s CLI is close enough to Docker’s that most muscle memory and most existing scripts carried over directly. Where it actually goes further is systemd integration through Quadlet: a container becomes an ordinary systemd unit, managed with the same systemctl and journalctl habits as everything else on the box, instead of a separate runtime with its own lifecycle tooling bolted on the side.

It’s the same instinct behind treating a laptop the same way as a node in a cluster: fewer standing daemons, fewer special cases, fewer things running as root just because that used to be the default.

AI pair

and OpenCode for day-to-day engineering; purpose-built agent harnesses where a workflow needs one.

Secrets

as the source of truth, injected into environments at runtime (via fnox + age here) and never committed.

Shell & OS

bash and Linux everywhere, a laptop managed the same way as a node in a cluster.

Bash over Fish

Same shell, everywhere.

Fish is a genuinely nicer interactive shell out of the box. Autosuggestions, sane defaults, syntax highlighting without any configuration. I’ve used it, and I understand why people switch and don’t look back.

The problem is everywhere else. Every server I SSH into, every CI runner, nearly every container base image, defaults to bash or plain sh, not fish. Fish scripts aren’t POSIX compatible, so anything written for interactive use in fish still has to be rewritten in bash (or sh) the moment it needs to run unattended somewhere else: a cron job, a CI pipeline, a Dockerfile RUN line. That’s a second shell dialect to maintain for no real benefit.

Bash isn’t the nicer shell to type into. It’s the one that’s already there, whether I’m sitting at a laptop or SSHed into a node three time zones away, interactively and in a script, with nothing to translate between the two. That’s worth more to me than better tab completion.

Workflows

Infrastructure as code

for configuration, Terraform / OpenTofu for provisioning, Packer and cloud-init for images. If it is not in a repo, it is not real.

GitOps

reconciles cluster state from Git; CI/CD runs in Gitea Actions on a self-hosted forge.

Reproducible builds

Projects declare their toolchain and build in containers, so a checkout is enough to build and run. This site is a small example: one mise run start renders a Containerfile and serves the app.

Observability first

Grafana, Loki, and Prometheus (with Grafana Alloy) go in early, not after something breaks; Gatus and ntfy cover health and alerting. Umami to understand how visitors interact with my sites.

Compliance as a constraint

NIST 800-171 / CMMC 2.0 requirements shape the architecture from the start rather than being bolted on.

Tech stack

AI infrastructure

Agents, retrieval, and the tooling that wires them into real workflows.

AI agents
Claude Code

Anthropic's agentic coding tool, run from a terminal or IDE to read, edit, and operate on a real codebase.

Open full page →
OpenCode
Hermes Agent
Retrieval & tooling
RAG pipelines

Retrieval-Augmented Generation — grounding an LLM's answers in retrieved documents instead of memory alone.

Open full page →
MCP tool integration

Model Context Protocol — an open standard for connecting an AI agent to external tools, data, and systems.

Open full page →
Execution & runtime
Agent sandboxing
Containerized job orchestration
Open WebUI

Application & solution architecture

How systems get designed and decomposed before anything is built.

Design & standards
API design
Design review and architecture standards
System design for scale
Modernization
Service decomposition
Monolith-to-modular modernization
Integration patterns
Application patterns
Workflow-oriented applications

Languages

What I actually write code in, day to day.

Scripting & systems
Python
Bash
C
C++
Rust
Application & enterprise
TypeScript
C#
Java
Other
Lisp
TCL

Cloud & platform

Where workloads run, from public cloud to a self-hosted Kubernetes cluster.

Public cloud
AWS
Azure
Hetzner Cloud
Cloudflare
Virtualization & bare metal
Proxmox
QEMU
systemd
Kubernetes ecosystem
Kubernetes

A container orchestration platform for deploying, scaling, and self-healing distributed workloads.

Open full page →
K3s

A lightweight, single-binary Kubernetes distribution built for edge, IoT, and small-footprint clusters.

Open full page →
Helm
Docker
Podman

A rootless, daemonless container engine, compatible with Docker CLI and images.

Open full page →
Podman over Docker

Docker’s daemon runs as a persistent, typically root-owned background process — anything that can reach it can effectively act as root on the host. Podman is rootless and daemonless by default: containers run as regular child processes under the invoking user, with no standing privileged service to compromise. On systems that also carry NIST 800-171 / CMMC 2.0 obligations, that’s not a nice-to-have, it’s one less thing to justify in an audit.

The switch is low-friction because Podman’s CLI is close to a drop-in replacement for Docker’s — most muscle memory and most existing scripts carry over directly. Where it goes further is systemd integration (Quadlet): a container can be defined as an ordinary systemd unit and managed with the same systemctl/journalctl habits used for everything else on the box, rather than a separate container runtime with its own lifecycle tooling bolted on the side.

That’s the same principle behind treating a laptop the same way as a node in a cluster: fewer special cases, fewer standing daemons, fewer things that behave differently depending on which machine they’re running on.

Traefik
Network & access
Bind9
Apache Guacamole

A clientless remote desktop gateway — RDP, VNC, and SSH access through nothing but a browser.

Open full page →

Automation & IaC

Configuration, provisioning, and delivery, all declared in a repo.

Configuration & testing
Ansible

An agentless configuration-management and automation tool driven by declarative YAML playbooks.

Open full page →
Ansible Molecule
Provisioning
Terraform / OpenTofu
Packer
cloud-init
Delivery & tasks
mise

A tool version manager and task runner that pins languages, tools, and tasks per project.

Open full page →
Argo CD

A declarative GitOps continuous-delivery tool for Kubernetes — the cluster's actual state is reconciled from Git.

Open full page →
Gitea Actions
QA
Playwright

A browser automation framework for end-to-end testing across Chromium, Firefox, and WebKit.

Open full page →

Observability

Metrics, logs, dashboards, and alerting, wired in from day one.

Metrics
Prometheus
Grafana Alloy
InfluxDB
Logs
Loki
Graylog
Dashboards & alerting
Grafana
Gatus
ntfy

Identity, endpoint & PKI

Directory, SSO, and certificates for every identity in the environment.

Directory & endpoint
Active Directory
Microsoft Intune
OpenLDAP
SSSD
SSO & secrets
Authentik SSO with enforced MFA and forward-auth
1Password

A password manager used here as the source of truth for secrets, injected into environments rather than committed.

Open full page →
PKI & certificates
step-ca private CA
cert-manager
Certbot / Let's Encrypt

Data & storage

Where state lives and how it's queried.

Databases
PostgreSQL
CloudNativePG
Redis
Storage & forge
SeaweedFS
Gitea
Workflow/data tooling
n8n

Security & compliance

Frameworks and hardening that shape the architecture, not bolt onto it.

Compliance frameworks
NIST 800-171
CMMC 2.0 (Levels 2–3)
Hardening
Kubernetes hardening (Pod Security Admission, audit policy, etcd encryption at rest)
Network defense
fail2ban
firewalld / UFW
WireGuard

Delivery & collaboration

How work gets tracked and communicated.

Issue tracking & docs
Jira
Confluence
Comms
Slack
Teams

Colophon

This site is a static Astro build, served as a container in development or via nginx in production, all driven by mise tasks. The editor config wires astro-ls into Helix, and the theme system is generated from the Helix project's bundled themes.