← Back to blog

From Scattered Scripts to a Sandboxed Catalog: How Sindri Came to Be

21 July 2026 · 6 min read

self-hostedsecurity

Not an app built to a plan from scratch, but a tool that emerged out of a real problem and grew according to whatever using it revealed was missing.

Where the app came from

Sindri didn't start as a project. It started as a side effect of routine maintenance after an outage — while cleaning up homelab scripts scattered across multiple machines, a question came up that sounds familiar to almost anyone managing more than one server: where's that script I wrote a month ago, what exactly does it do, and which machine did I last see it on?

Instead of a manual list in a single .md file (which had already caused exactly this kind of failure once), an app was born: a FastAPI + SQLite backend, a React frontend, full-text search that reaches into the scripts' actual content, and — from day one, not bolted on later — detection of hardcoded passwords and tokens in imported code.

Rapid growth, driven by real use

Most apps grow according to a feature plan written in advance. Sindri grew differently — every round of work added whatever real use revealed was missing, and almost every round turned up at least one genuine, live-verified bug along the way:

  • Safe remote script execution over SSH to registered machines, with the sudo password sent over stdin (never stored), tested for real against actual machines — including discovering that one of them has sudo physically bound to a hardware key (an intentional property of that machine, not an app bug, but the app had to handle it gracefully).
  • Sandboxed execution — an isolated Docker container with no network, a read-only filesystem, and time and memory limits. Tested live against a real Docker daemon: writing outside the allowed space failed correctly, a fork bomb was stopped within a second, and an artificially allocated 500 MB correctly triggered an OOM-kill.
  • AI-assisted review and generation of scripts via local CLI tools (with a fallback to a direct API), which on its very first real test correctly flagged a test password hardcoded in the code — exactly what it was supposed to catch.
  • Content history with rollback, an execution history pulled from the audit log, bulk tag editing, a command palette (Ctrl+K), a full catalog export, and login rate-limiting with a per-IP lockout.

Several of these bugs weren't cosmetic. Double-clicking "Add to catalog" created two identical entries at once (a missing guard against duplicate submission). The copy button silently didn't work, because the app ran over plain HTTP on a LAN address and the browser's Clipboard API requires a secure context — the app looked fine on the surface, nothing just got copied. A generated SSH command for copying pointed at the SSH key's path inside the container, which didn't exist at all on the host machine — the copied command would have failed in a real terminal.

The common thread across all of these findings: none of them turned up just from reading the code. Every one required actually trying the app — through a headless browser, through curl against the running API, or directly against real data (an existing script on disk, a real SSH key, a real target machine).

The decision to go public — and what came before it

Once the app grew to a point where going public on GitHub made sense, a phase began that can be summed up in one sentence: an app that works on your own machine isn't automatically an app that's safe to hand to someone else.

Concrete steps before the first public push:

  • A full audit of the git history and the code for personal data, passwords, IP addresses, and emails — nothing turned up, but the check was done systematically, not just assumed to be fine.
  • A real portability test: a fresh export of the repo deployed on a completely different machine (different architecture) showed the 29 generic templates the library had at the time (it's grown since — see below) and zero personal scripts — confirming the app genuinely didn't assume the presence of any specific environment.
  • A sandbox bug found right before the audit: a missing group permission for the non-root container to access the Docker socket meant the sandbox wasn't actually working the way its own documentation claimed — fixed and verified live before the app was even considered as something someone else might see.
  • Extra security findings beyond the original review — restricting which paths the app is allowed to import scripts from (against potential path traversal), and server-side validation of SSH paths instead of relying on the client side alone.
  • 28 automated tests actually run (not just written), a first GitHub Actions CI run, a rewritten README with its own security-model and known-limitations sections, and a standalone SECURITY.md.

The repo stayed deliberately private for a while even after it was ready — the decision to publish was entirely mine; the app was just sitting there, prepared.

Why the app is built for beginners, not just for me

An interesting detail behind the whole project: the reason the app ships a fairly large library of built-in generic templates (well over a hundred today) isn't just convenience. I learned Linux on my own, without a mentor — which is exactly why the templates target beginners too, not just an experienced admin who can already write anything from scratch.

What to take away from this build

  • An app born out of one annoying problem tends to be more useful than one designed up front from a feature list — every addition responded to something that actually happened during real use.
  • Security checks belong there from the start, not as a last step before publishing. Hardcoded-password detection was part of the app from day one; even so, more layers (path containment, server-side validation) were added before it went public.
  • "Tested" should mean actually tried, not just read. Most of the real bugs in this project turned up exactly when someone actually used the app — through a headless browser, through curl, against real data — not while reading its own source code.
  • Being ready to publish and actually publishing are two separate decisions. An app can be technically ready (a clean audit, a scrubbed history, passing tests) and still stay private until I decide otherwise.

Sindri today runs as a self-hosted catalog of personal scripts with AI-assisted review, a sandbox, and remote execution — it started as a footnote to a different problem and became one of the most thoroughly reviewed projects in the whole homelab.

The same pattern — build it, test it for real, verify before publishing — repeats in Muninn (document archiving) and MidgardOps (the homelab operations center).