Case study / the workflow behind everything else on this site

ai-workflow-kit

Updated Oct 30, 2026 · v3.3.0 · MIT · github.com/Phoenixn82/ai-workflow-kit

Every project on this site was built with Claude Code and Codex working side by side. This repo is that setup, exported and scrubbed: the hooks, the command-line tools, the model routes, the memory pipeline and the server recipes, plus an installer that puts only the parts you pick on your own machines.

I run my agents without permission prompts, because stopping to click "allow" a few hundred times a day is not a workflow. That only works if the safety lives somewhere other than the prompt. Most of this kit is the answer to that one question: what has to be true, in code, so an agent can run unattended and still never print a secret, push private data to a public repo, or quietly burn through a week of usage on the wrong model?

# what this machine already has, then the plan, then the install
$ install/doctor.sh --profile odin
$ install/install.sh --profile odin,telegram --dry-run
$ install/install.sh --profile odin,telegram

# which model and effort each kind of job gets
$ KIT_HOME=$PWD framework/harness/bin/route list
class                  lane             model             effort
feature-build          claude.subagent  claude-opus-5-5   high
long-unattended-build  codex.build      gpt-6.1-sol       high
security-review        codex.plan       gpt-6-astra       xhigh
bulk-sweep             claude.sweep     claude-haiku-5-5  medium
private-data           local.text       qwen3.8-text:27b  n/a
Real commands from the README. The route table is trimmed to five of its 17 rows.
The problem

Rules written in a prompt file get ignored.

My first setups were piles of instructions: a long rules file, a growing list of skills, notes telling the model which tool to use for what. They drifted. When I audited the whole workflow in October 2026, the numbers were plain. One web-tools rule was written in 8 places and still missed 6 times. The memory index had been overwritten and 142 of 185 memories went dark for 22 days. About 75k tokens loaded before the first prompt of every session, and every subagent paid about 54k of that again.

The same audit showed where things worked. The day that web rule became a real command-line tool registered as a skill, it got about 1,900 calls in 12 days and the banned tools dropped to zero. So the kit follows one idea: when a rule matters, ship it as a hook, a CLI, a lint or a test. Keep the instruction files short, and put the rest in code that runs whether or not the model remembers.

What it does

Six machine setups, one installer.

The kit is organized as profiles, one per job a machine can have. Each profile is a profile.json that lists tool ids from one manifest, install/tools.json, plus the services, timers and rules that machine runs. The names come from my own home lab, and they are just role names here.

COCKPIT

odin

The laptop where Claude Code and Codex run: the hook dispatcher and secret firewall, the public-push gate, the model routes (route, cx), the web CLI, the vault with prompt-time search, automatic session memory, a weekly review, a harness lint and a CPU ceiling for agent fan-outs.

ALWAYS ON

heimdall

An old laptop as a server: uptime checks that know when a box is asleep on purpose, Wake-on-LAN, a jobs registry with kill switches, the vault-search embed server, a backup target and a deploy kit (Compose, Caddy or Tailscale Serve, a systemd unit, a health check, a restore script).

GPU, MEDIA, VIEWER, PHONE

thor, loki, raven, telegram

A GPU box for Ollama and Whisper that sleeps when idle and wakes on demand, a media server behind a one-way firewall, a thin client that only connects, and a Telegram add-on where every alert is silent except heat warnings.

Nothing installs until you confirm. Every file the installer replaces gets a timestamped backup beside it, and your agent config is only merged when you pass --harness. Third-party tools are downloaded from their official source at install time, never copied into the repo.

How it works

Agents load skills, skills call tools, tools run on machines I own.

Every tool call goes through one dispatcher. Claude Code's PreToolUse hook starts a single Python process that runs each before-hook whose matcher applies, then merges the answers: deny beats allow, and every reason is kept. Moving five hooks into one interpreter halved the hook cost of each Bash call, from about 0.3 to 0.15 CPU-seconds. If a safety hook crashes, times out or prints something unreadable, the call is denied. An ordinary hook that fails is just dropped.

The secret firewall reads operands, not raw text. It treats a file as read only when it is the argument of a read verb, the source of a copy, a redirect, or a path opened inside python -c, a heredoc or an ssh command. That means rg input_tokens passes while quoting tricks, hex escapes and variable indirection around a credential file get caught. It is scored against a 182-case replay corpus (96 must-block, 86 must-allow), and the docs list what it cannot catch right next to what it can. It is a guard against accidents, not a security boundary, and the docs say so.

One table decides models. lanes.json maps each kind of job to a lane, a model and an effort level. route prints it, cx dispatches Codex from it, and a route-guard hook denies any subagent call that names no model, so nothing silently falls back to the most expensive default.

Memory writes itself. At the end of every session, a distill step writes a short handoff into a plain-markdown vault. On every prompt, a hook searches that vault and injects the few most relevant notes. The embedding server lives on the always-on box, so search does not fall back to keyword-only when the GPU box is asleep.

The repo tracks what actually runs. Everything under framework/ is exported from my live setup by tools/sync-kit.py: an allowlist of files, a scrub pass, a syntax check and a secret scan. A pre-commit hook in the repo scans every staged file for secrets and private terms and refuses author emails that are not GitHub noreply addresses.

One tool call, start to finish 01 Agent Claude Code / Codex no permission prompts 02 Dispatcher secret firewall · cred guard model guard · route guard 03 Tools route · cx · web vault · lane · tg 04 Machines over Tailscale hosts.env, no defaults safety hook fails → call denied A git push to a public repo, or a private-to-public flip, also runs the public-push gate: secret scan over every blob in history, a personal-data denylist, then a read-only Codex review.
The always-on layers. Each one fails closed.
What was hard

Making "fail closed" actually mean closed.

The public-push gate is the piece I rebuilt the most. Its job sounds simple: before anything reaches a public GitHub repo, scan it for secrets and for my own personal data. The hard part was every way a scan can quietly cover less than you think.

// ways a "passing" scan can lie

A file added in one commit and deleted in the next is gone from the tree but still in the history. A secret inside a zip or a PNG never shows up in a text grep. A file named like a test fixture gets skipped by path rules. A scanner that hits an error can exit 0. And an override meant to skip one slow step can end up skipping all of them.

So the gate scans every blob version in the pushed history, under neutral file names so no path rule can skip it, with archives unpacked and printable strings pulled from binaries. It passes its own scanner config with --config, so a .gitleaks.toml committed to the scanned repo cannot weaken it. A scanner error, a partial scan, an unreadable blob or a missing denylist blocks the push. The one-time override skips only the Codex review; the secret scan and the denylist always run.

The second hard thing was keeping the model choices honest. At one point the main session's model flipped 15 times in 41 days, because an interactive model switch rewrote the global default and every subagent inherited it. The gate itself once pinned a retired model, so every public push failed. That is where lanes.json, apply_lanes.py and the route guard came from: one table, one script that writes it into every tool's config, and a lint that checks the live configs against the table.

The third was tests. A test that imported the memory pipeline before its fixture set the temporary paths wrote test notes into my real vault, twice in one day. The fix went into the code, not the test: the pipeline now refuses to import under a test runner until its state and vault paths are set.

Results

Tested offline, on every push.

A GitHub Actions workflow runs the manifest, profile, docs and lanes checks, the offline self-tests of the bundled tools, the installer test, a macOS setup test in containers and gitleaks on every push and pull request. These are the numbers from a fresh clone on Oct 18, 2026, run against a throwaway home directory:

76/76installer test checks
84/84public-push gate end-to-end checks
46tool self-tests pass, 4 skip, 0 fail
182firewall replay cases, 0 misses
86tools in the manifest, 41 bundled
32/32broken manifests caught by the linter

The four skips are tools that need a live systemd user session or the optional sqlite-vec extension. The firewall the corpus replaced caught 13 of 52 cases; the current one has 0 misses and 0 false blocks, and on 10,000 real commands it denies 0.23 percent of them.

The less countable result: when a rule now matters to me, my first question is which hook, CLI or lint should hold it, not where to write it down.

Stack

Bash, Python stdlib, and a lot of tests.

The code is mostly bash and Python 3 using the standard library first, with a little JavaScript. There is no framework and no build step. Each bundled tool carries its own offline self-test, and tools/run-selftests.sh runs all of them with an empty environment.

Languages
bashPython 3 (stdlib first)JavaScript
AI
Claude CodeCodex CLIOllamaoptional free-model router
Safety
hook dispatchersecret firewallbetterleaks / gitleaksHALT kill switch
Infra
systemd user unitsDocker ComposeTailscaleresticUptime KumaJellyfin
Install
pacmanaptHomebrewmiseuvnpmDocker
CI
GitHub Actionsgitleaks-actionpinned action SHAs
Uses · updated Oct 2026

What I build with.

The machines, AI tools and services this workflow runs on.

Machines

  • ASUS ROG laptopMain machine, running Omarchy (Arch Linux with Hyprland). This is where the AI coding sessions run.
  • Debian GPU serverRuns local models with Ollama, plus backups and heavy jobs like video renders and transcription.
  • Always-on mini serverAn old HP laptop running headless Debian. It handles scheduled jobs: the job-board scanner, a daily video pipeline, uptime checks.
  • 2019 MacBook ProAlso on Omarchy. A thin client I type from.
  • TailscaleA private network that ties the four machines together.
  • Bambu Lab P1S3D printer, for hardware projects and parts.

AI

  • Claude CodeMy main coding agent.
  • OpenAI CodexThe second coding agent. Long builds go here, and each agent reviews the other's work before it ships.
  • OllamaLocal models on the GPU server, for anything private that shouldn't leave my machines.
  • Obsidian vaultLong-term memory. Notes from every AI session get written into it automatically.

Shipping

  • GitHubEverything starts in a private repo. A push gate scans for secrets and personal data before anything can go public.
  • VercelHosting for this site and the client sites.
  • Next.js, Astro, plain HTMLWhatever the project needs. This site is hand-written HTML with no build step.
  • SupabasePostgres when a project needs a database.
  • PythonScripts, data pipelines and the scanners.