Superpowers: Giving Your Coding Agent a Workflow It Can’t Skip

If you have used Claude Code, Cursor, or Codex for real work, you know the pattern. You ask for a feature, and the agent immediately starts writing code. No spec. No plan. No tests. Twenty minutes later you have something that looks right and is subtly wrong.

The fix is not a smarter model. It is a better process. That is exactly what Superpowers is.

What it is

Superpowers is an open-source software development methodology for coding agents, built as a plugin around a library of composable “skills.” It works with Claude Code, Codex CLI, Gemini CLI, Cursor, GitHub Copilot CLI, and a few others.

The key idea: the skills trigger automatically. You do not have to remember to invoke anything. The moment the agent sees you are building something, it stops and runs a real workflow instead of guessing.

It is MIT licensed and, at the time of writing, has over 190k GitHub stars.

How the workflow actually runs

When you ask Superpowers-equipped agent to build something, here is what happens instead of an immediate code dump:

  1. Brainstorming. Before any code, it asks questions and refines your rough idea into a written spec. It shows you the design in chunks short enough to actually read.
  2. Git worktrees. Once you approve the design, it creates an isolated workspace on a new branch and verifies a clean test baseline.
  3. Writing plans. It breaks the work into bite-sized tasks of roughly two to five minutes each. Every task has exact file paths, the code to write, and verification steps.
  4. Subagent-driven development. It dispatches a fresh subagent per task, with a two-stage review: first does the work match the spec, then is the code actually good.
  5. Test-driven development. Strict RED-GREEN-REFACTOR. Write a failing test, watch it fail, write the minimum code to pass, watch it pass, commit. Code written before its test gets deleted.
  6. Code review and branch cleanup happen between tasks, with critical issues blocking progress.

The plans are written to be clear enough for, in the project’s own words, an enthusiastic junior engineer with no context and an aversion to testing. That is the bar. If a plan is that explicit, an agent can follow it for a long stretch without drifting.

Getting started

For Claude Code, install from the official plugin marketplace:

/plugin install superpowers@claude-plugins-official

For other harnesses (Codex, Gemini CLI, Cursor, Copilot CLI), the repo has a one-line install for each. If you use more than one agent, install it separately for each.

That is the whole setup. After that, the skills just fire on their own.

The philosophy in one line

The project rests on four principles: test-driven development always, systematic process over ad-hoc guessing, simplicity as the primary goal, and evidence over claims. Verify before declaring success.

None of this is new advice. What is new is having an agent that enforces it instead of an agent that ignores it.

Is it useful?

Yes, with one honest caveat.

It is genuinely useful if you do non-trivial work and have been burned by an agent confidently shipping broken code. Because the agent follows a plan you already approved, it can run autonomously much longer without going off the rails. The structure is the value.

The caveat: it is deliberately slower for trivial work. If you just want to rename a variable or fix a typo, the brainstorm-then-spec-then-plan ceremony is overkill. Use Superpowers for features and real changes, not one-line fixes.

The takeaway

The model is rarely the bottleneck anymore. The bottleneck is that agents skip the boring, disciplined parts of engineering. Superpowers makes those parts mandatory. If your agent keeps producing plausible-looking garbage, this is the cheapest fix available: it is free, it installs in one command, and it changes nothing about how you work except the quality of the output.

Frontend with AI: workflow first, agent second

Workflow anatomy: a six-phase timeline, the TWD live loop, and the four non-negotiables

If you’re about to plug AI into your team’s frontend, define the process before you let it loose. Without a workflow, what gets faster isn’t delivery. It’s technical debt.

At Orbitant we run a development consultancy, and we’ve spent months refining a concrete workflow for integrating AI into the day-to-day of the frontend teams we work with, adapting it to each client’s stack and constraints. The idea isn’t new, but the nuance matters: AI only amplifies what your team already does. If you already had a quality process, speed goes up and quality holds. If not, what goes up is the amount of code without tests, without context, and without review.

That was the starting point.

The problem: AI amplifies what you already have

Before AI was part of the flow, in any tight sprint the usual story played out: focus on the happy path, push tests to next week, close the ticket. AI didn’t change that. It sharpened it. More code per hour meant more uncovered paths, more untested components, more silent decisions nobody was going to review.

A team figures this out fast. The problem isn’t the tool. It’s that there’s no mandatory process the AI has to respect.

The proposal: define the workflow first

The opening rule is simple: if we take AI out of the picture, quality, relative speed, and regression control all have to stay exactly where they were. AI on top of a good process amplifies results. On top of a broken one, it amplifies problems.

So first you define the path. Then you build a Claude Code skill that orchestrates that path step by step. Everyone goes through the same hoop. With or without AI.

The piece that makes it click: TWD

A workflow like this only works if the tool running the tests also respects the process. TWD (Test While Developing) does exactly that: it runs the tests inside your own dev server, against the same DOM and the same mocks you’re looking at right now in the browser. No simulations, no jsdom, no separate Chrome under the hood.

That fits with AI for a very specific reason: results come back as text. The agent reads whether the test passed or failed, reads the error if there is one, and retries. No heavy screenshots, no DOM dumps. The loop costs few tokens, so it’s viable to repeat it hundreds of times a day without the bill exploding. And test quality goes up because the tests stop being “a component mounted in isolation”. They become what the user is going to see in the real app, with its real components and real interactions.

The flow, phase by phase

Workflow anatomy: a six-phase timeline, the TWD live loop, and the four non-negotiables

It’s six phases, but in practice it simplifies to four mental blocks:

  1. Understand the ticket properly. Figma, screenshots, brainstorm if the feature is complex. The clearer the start, the better the implementation. The skill can pull from the Figma MCP when there’s design, and break out into a separate brainstorm if the thing looks fuzzy.
  2. RED → GREEN, non-negotiable. Tests come first, from the requirements. When a test fails, you fix the implementation, never the test. No guess-coding. If after three attempts a test is still red, mark it as it.skip with a TODO. Never silence.
  3. TWD live loop. The part people like most when they see it for the first time. The agent sends tests to the browser tab you already have open: no separate Chrome, no new window, no simulated environment. Your tab, your app, your mocks. You watch the clicks, the forms, and the redirects happen in real time while the agent reads the results as text and decides the next step. (The technical piece connecting agent and browser is called twd-relay; details at twd.dev for anyone who wants them.)
  4. Reviewer. Before closing, a subagent runs lint, build, and the full suite. If anything breaks, back into the loop. The feature isn’t done until everything is green.

The bottleneck moves. From “writing code” to “deciding what you want and reviewing what comes out”. Which is exactly where we want it.

The rules for structuring tests

The workflow is the kitchen; the rules are the recipe. These are the four non-negotiables that come up in any team conversation:

  • Test what you own, mock what you don’t. Tests cover what you control. What you don’t (third-party iframes, external SDKs, cross-origin widgets) gets mocked, documented as out of scope, and covered from the QA layer with real E2E or manual tests. It doesn’t disappear, it gets tested somewhere else.
  • Flow-based tests, not element-based. Each it() covers a complete journey: visit, interact, assert the outcome. One test per element is noise, not signal.
  • Test first, code second. Non-negotiable, even when “it’s just a small change”. Small changes break the system; tests take minutes.
  • A test that passes without an implementation is suspect. Either it’s testing pre-existing behavior (justify it) or it’s testing nothing. Either way, look at it.

These rules didn’t come from a manual. They came from looking at what broke in previous sprints and putting a brake at each point.

Closing

If your team is about to put AI into frontend, the real decision is this: what process do you want the AI to amplify?

An agent without a process is a developer in a hurry. An agent inside a workflow is a colleague that respects the same rules as the rest of the team. The difference shows up the first time a regression doesn’t reach production.

Looked at from outside, this is harness engineering applied to a software process: the harness doesn’t just call the model, it enforces a workflow the team already considered good before AI showed up.

first the failing test → then the most obvious implementation that passes

Comprehension Debt: The Hidden Cost of AI-Generated Code

Comprehension Debt: The Hidden Cost of AI-Generated Code

“You’re not a developer anymore. You’re a reviewer of code you don’t understand.”

That line from The Serious CTO named something I’d been feeling but couldn’t articulate. With AI, the volume of code we ship has completely decoupled from the amount of code any human actually understands.

I call this comprehension debt — the gap between the code in our systems and the knowledge in our teams’ heads. And I believe it’s the most important kind of debt our industry has accumulated in the past two years.

It’s not technical debt

Ward Cunningham’s original “technical debt” was a conscious trade-off. Comprehension debt is different.

It isn’t deliberate.

It isn’t local.

And it doesn’t show up on any dashboard.

What the dashboards actually measure

DORA, Flow, and even the new AI Impact metrics all measure motion — speed, volume, acceptance rates, deployment frequency. None of them measure whether anyone on the team can still explain the code they shipped last quarter.

The standard answers fall short

More code review, more linters, more tests, and “future AI will fix it” don’t solve comprehension debt. They mostly create the illusion of safety.

What it looks like in practice

Fields nobody can explain, controllers with dead branches, 2am incidents where the on-call engineer can read the stack trace but can’t narrate the logic. Individually minor. Collectively, the new shape of legacy code — built at unprecedented speed.

What should we do instead?

I don’t have perfect answers yet, but I know the solution isn’t “more of the same.” We need to actively maintain comprehension as a first-class team property.

Originally published on my blog:

https://davidslv.uk/2026/05/17/comprehension-debt.html

What do you think?

Have you seen comprehension debt in your own teams? What (if anything) are you doing about it?

Building a linter for the bugs AI agents actually make

I lost an hour last Tuesday to a function that didn’t exist. The agent had written what looked like fine Postgres code, db.QueryRowContext with a context and a query string and a couple of args. It compiled. Wouldn’t run. Took me forty minutes to work out it had used db.QueryRow (no context, different signature) inside something it called QueryRowContext, and was handing five things to a function that wanted three. The build error was clear enough, in hindsight. What wasted my hour was that it looked like a hundred other build errors I’d seen, and I kept reading it as a typo I could fix in two seconds.

There’s a number that keeps making the rounds, that a majority of developers now say they spend more time debugging code their AI assistant wrote than debugging code they wrote themselves. I’d argue with the methodology if I didn’t feel it in my own week.

Sitting with the Tuesday bug, I started cataloging. It had a shape, and so did most of the bugs I’d been hitting. Hallucinated method names. Right name, wrong arity. Right arity, wrong types. A constant that got renamed three versions back and now exists only in the agent’s training data. They cluster. They’re not random. So I went looking for a Go linter that catches them, and when I couldn’t find one I wrote it.

It’s a Go CLI called vouch. The first thing it does, the thing that’s working as of this week, is read the output of go build and tell you whether your failure looks like an AI hallucination or a normal-person bug. That distinction matters more than I’d expected, and the way I built the detector is dumber than I’d expected, so this is about both.

Go already has good linters. staticcheck is sharp, golangci-lint bundles two dozen analyzers and is the standard at every company I know. They catch real bugs. What they don’t catch is “your AI assistant called db.WithTimeout() and that method doesn’t exist.” That’s a build failure, not a lint failure, and by the time the linter runs the compiler has already given up. For a human writing Go, build failures are usually typos. You fix them in five seconds and barely register them as bugs. For AI-written Go, build failures are the most common bug class by a wide margin, and they cluster into the four shapes above. You can see all of them in a single go build output, sitting next to each other, indistinguishable from a missing import. What vouch does is pull them out and label them.

I wanted the first detector to be useful without being clever, so it isn’t. It’s a screen scraper. It shells out to go build ./..., captures stderr, parses each line against a small set of regular expressions, and bins the error into one of four categories: undefined-symbol, undefined-method, arity-mismatch, type-mismatch. No language server, no AST walking, no model in the loop. go build and regex. The regex was ninety minutes of work. Most of the day went into the test fixtures, which is the same proportion every tool I’ve built has settled into.

The piece that actually matters is the --diff flag. It narrows the report to lines you changed:

$ vouch check . --diff main
internal/store/user.go:42: arity-mismatch
  ctx.WithTimeout(5 * time.Second) called with 1 arg, expected 2
  func WithTimeout(parent Context, timeout Duration) (Context, CancelFunc)

That’s what turns vouch from “tell me everything wrong with this codebase” into “tell me what my agent just broke.” Without the diff scope, it’s noise. With it, it’s a five-second pre-PR check.

I want to head off the obvious counterargument. A lot of the AI-code-review tooling I’ve seen does the obvious thing, throws the diff at a model and asks the model what’s wrong. Sometimes that works. It also costs money per invocation, takes a few seconds per file, and gives you a different answer every time you run it. Deterministic checks are free, instant, and reproducible. If you’ve called ctx.WithTimeout(5 * time.Second) with one argument I don’t need a frontier model to tell me you forgot a parent context. I need go build and a regex. The plan from here is to layer gopls on top for the cases the compiler alone can’t catch (wrong arg order on signatures that happen to type-check, deprecated APIs), and only reach for a model at the very end, narrowed to a region the cheap checks already flagged. That’s the inverse of how most of this tooling is shaped today, and the inverse is right.

The bug that actually pushed me from thinking about building vouch to building it wasn’t even in the four-bucket bin. I was helping an agent put together a small Go service, and it produced this:

db, err := sql.Open("postgres", dsn)
if err != nil {
    return err
}
defer db.Close()

rows, err := db.QueryContext(ctx, "SELECT id FROM users")
if err != nil {
    return err
}
defer rows.Close()

Compiles. Runs. Wrong in a way I didn’t catch for thirty minutes, because there’s no db.Ping() after sql.Open. The first failure mode isn’t a connection error at startup, it’s a panic deep inside QueryContext. Classic. Mirrors a thousand Stack Overflow examples but skips the half they leave implicit. vouch doesn’t catch this one yet. It’s a pattern-incompleteness bug and it lives further up the difficulty curve. I started with the four-bucket detector because building real coverage on the easy class first is how you find out if the rest is worth chasing.

Which gets to the part nobody else is doing. If I tell you my AI linter catches AI bugs, you should ask me how often. Precision, recall, false positive rate against an off-the-shelf staticcheck pass on the same code. Every “I built an AI code reviewer” project I’ve come across skips that question. They show one screenshot of one bug. They don’t tell you how often the tool cries wolf. The next thing I’m building isn’t the next detector, it’s a real eval harness, fifty-plus real-world AI-authored PRs pulled from public GitHub history (you can find them by searching for the Co-Authored-By: Claude trailer, Cursor’s metadata, Devin’s PR titles, or Sweep’s signature), labeled for whether they introduced bugs, and a detection-rate number to put in the README.

I had a moment last week where I almost started the api-shape detector before I’d ever run vouch against a real codebase. Would have been a mistake. The thing that earns a tool the right to keep growing is showing that its first claim is actually true. Code’s at github.com/c-tonneslan/vouch.

What I learned opening my first sixty open source pull requests

Twelve days ago my GitHub account had zero contributions on it. Not zero this year, zero ever, because I’d deleted my old account in a fit of housekeeping and started fresh from a new email. Today there are about sixty pull requests open or merged across twenty-something repos in Go, Rust, Python, and TypeScript. About half are merged. A few got rejected (one for reasons I’ll get to). The rest are in review.

The version of this I want to write is the version I wish I’d read at the start. Not “fork the repo, read CONTRIBUTING.md, be respectful,” you already know that. The version where I tell you about the things I got wrong, often, in the order they happened.

The first thing I got wrong was assuming the “good first issue” tab was a green field. It isn’t. Day one, I opened the Tailscale repo, filtered “good first issue” to oldest first, picked the first one with a clean repro, and had a fix in an hour. Went to push the PR, ran gh pr list, and found there was already an open PR for it dated six weeks earlier, sitting in review. This kept happening. A third of the issues I picked off that tab had a draft PR somewhere. The ones that didn’t have a PR often had a maintainer comment buried in the thread saying “actually we don’t want to fix this” or “blocked on another redesign, don’t bother.”

What saved me was a single command I now run before touching anything:

gh pr list --repo OWNER/REPO --state all --search "<keyword from issue>"

If there’s already a PR you’ll see it. If there’s a closed PR with maintainer comments you’ll learn the project’s actual opinion on the issue, which is usually more useful than reading the issue itself. I skipped this once on a chi PR and ate a “duplicate of #1085, which was submitted first and merged today” comment within an hour. Annoying. Avoidable.

Read CONTRIBUTING.md before writing a line

The first PR I rushed without reading CONTRIBUTING.md was to a project with a mandatory PR template I hadn’t filled out. Their github-actions bot auto-closes any PR that leaves required fields blank. It auto-closed mine in five minutes. I wasn’t even on the page when it happened.

Same week, I got a PR rejected from rs/zerolog for a more subtle reason. I’d added a method that accepted a Go context.Context and made it available on subsequent log events from that logger chain. It looked like the obvious convenience; every other modern Go logging library has something similar. The owner replied within a few hours: this is semantically incorrect, the method name implies the receiver context is being decorated, your change actually modifies the receiver, that can break code that depends on the immutability of the chain. Closed. The code was correct. The test passed. The change lived inside a single function. None of that mattered, because I’d altered the contract of a public method in a stable library that other people depend on.

Two lessons from that week. Read CONTRIBUTING.md and the PR template before writing a line. Templates exist to save the maintainer’s time, fill them out completely. And don’t change the semantic contract of a public method in a stable library just because the change feels internally consistent. If you think a public API needs to evolve, file an issue first and let the maintainers decide. The cost of asking is small. The cost of having your refactor closed because nobody asked you to is bigger than just the time you spent.

The zerolog rejection is the one that really changed how I think about API stability. I now check whether a small refactor actually changes user-observable behavior, even if it doesn’t break the test suite.

Reading code with no agenda finds better bugs than the issue tracker

There’s an obvious play. Open the issue tracker, pick a labeled issue, fix it. That works, and it’s what I did the first few days. It’s also competitive, and limits you to bugs the maintainers have already triaged.

The PRs I’m most proud of came from reading code with no agenda. Two examples. The first was a stale-context bug in pgx, the Postgres driver for Go. I was reading the connection-fallback path because I wanted to understand how target_session_attrs=prefer-standby actually worked. Halfway through I noticed a ctx variable being shadowed inside a for-loop and then reused in a fallback branch where its deadline had already burned. Nobody had filed an issue. The maintainer merged the fix in a few hours. The second was a nil panic in Wails, the Go-to-frontend desktop framework. I was reading their app-startup path, saw that Application.Quit dereferenced an inner pointer that didn’t get assigned until Run, wrote a five-line program that triggered the panic, and shipped a one-line fix.

Both were hard to find from the issue tracker because nobody knew they existed. They came from reading. That’s the angle I’d push to anyone starting out. Pick a project whose code you actually use. Read one of its packages end to end without trying to fix anything. Take notes on everything that surprises you. Some of those will be bugs.

The other thing I had to retrain myself on was tests. Three early PRs sat untouched for days, and in every case the reviewer’s first comment was “can you add a test?” So I started doing it by default. Even for a one-line fix, mirror the existing test pattern in the package and add a case that fails before the fix and passes after. Don’t introduce a new test framework or assertion library to a file that’s been using t.Errorf for years; that’s an instant flag. And a regression test on its own is sometimes acceptable even when the fix needs more thought. I had a sqlx PR where the maintainer asked me to split the test out as a separate commit, and that landed first.

Commit messages in the project’s voice

I read a hundred-ish commits from golang/go, tailscale, and a couple of Charm repos before writing my first PR, just to internalize the rhythm. Go projects almost always use package/path: short verb-first description, lowercase after the colon, no trailing period, around fifty characters. The body, when there is one, explains why rather than what, in plain prose paragraphs (not bullets), wrapped at seventy-two characters. First-person is normal. Opinions are normal.

Here’s a real Tailscale commit body I think reads well:

The Engine watchdog wrapped every wgengine.Engine method call in a goroutine with a 45s timeout and crashed the process on timeout. It was added years ago to surface deadlocks during development, but the underlying deadlocks have long since been fixed, and even when it did fire it produced obscure stack traces (from inside the watchdog goroutine, not the original caller) without buying much.

Notice the personal history (“added years ago”), the opinion (“without buying much”), no bullet list even though it has multiple reasons. None of those things are hard on their own. Getting all of them in the same paragraph reliably is the part that takes practice.

Twenty PRs in, I noticed the quality of attention I was getting was uneven. Some maintainers reviewed within an hour. Some sat on PRs for a week. A few projects auto-closed mine before a human ever saw it. So I started keeping a list per-repo of “is this worth the next PR.” A repo earns its way on by responding within a few days, having maintainers who comment substantively rather than just merging or closing, and having an issue tracker that isn’t a graveyard. The names that consistently delivered fast useful review for me: Tailscale, the Charm projects (huh, log, bubbles, lipgloss), pgx. A handful of others I quietly stopped trying after a closure or two; enough of a pattern for me, not worth the activation energy to push back. Your list will look different. The point is to have one.

After fifty-something PRs, the ones that merged in under a day had basically the same shape. One logical change, not “fixed this and also cleaned up some imports while I was there.” A diff under fifty lines, ideally under twenty. A test that proves the bug. A commit message that explains the why in a paragraph of plain prose. A PR body that references the issue number and adds a sentence or two about how I found the bug. No reformatting of surrounding code. The PRs that sat for a week missed at least two of those. The ones that got rejected missed all of them.

If you’re sitting at zero contributions and feeling like the gap is too wide, the thing I’d say is what I wish someone had said to me. Pick one or two projects whose code you already use. Read one package end to end without trying to fix anything. Take notes. When you find something that’s actually wrong, file an issue first if the project asks for one, then write the smallest possible fix with a test, then write a commit message that sounds like you’ve worked on the project for years. Do that ten times before you let yourself think about volume. Almost every shortcut version of this (find good-first-issues, fix typos in docs, run a script that opens fifty drive-by PRs) has either been done by someone else or is the kind of thing that doesn’t teach you anything. The thing that teaches you and the thing that catches a maintainer’s attention are the same thing: showing that you read the code.

The Role of Platform Engineering in a Startup

Platform engineering sounds like a big-company thing. But I think every startup past 20 engineers needs a small platform function. Here’s why.

The problem platform engineering solves

At 5 engineers, everybody knows how to deploy. At 20, they don’t. People start copy-pasting deployment configs, breaking things, and asking the same questions in Slack every day.

You need one person who owns ‘how we ship software,’ even part-time. That person is a platform engineer whether you call them that or not.

What a startup platform function does

1. Owns the deployment path. One golden path from git push to production. Documented, maintained, and defended from one-off exceptions.

2. Owns the dev environment. Laptop setup, local testing, shared services. New hire productive in days, not weeks.

3. Owns the shared services. Auth, logging, tracing, secrets management. Used by everyone, owned by no one until you assign it.

4. Owns the developer experience. CI speed. Local/prod parity. Error messages. The stuff that’s no one’s job but costs everyone.

What it doesn’t do

It doesn’t build a K8s abstraction layer that rivals AWS. Startups can’t afford that. Use off-the-shelf. Customize lightly.

When to start

At 15-20 engineers, if you’re still asking ‘why is my build failing’ in Slack 3 times a week, you’re ready.

Before 15, just have engineers fix each other’s stuff and take turns being the ‘ops person.’ It’s not efficient, but it’s cheaper than a full function.

The hire

Hire someone who loves developer experience. Not the best infrastructure engineer on the team the one who genuinely cares about making everyone else faster. That’s a different skill set.

Written by Dr. Samson Tanimawo
BSc · MSc · MBA · PhD
Founder & CEO, Nova AI Ops. https://novaaiops.com

How I Built a Lightweight Kg to Lbs Converter Firefox Extension

Well, In this project, I wanted to create a very lightweight and fast Firefox extension focused on one simple task: converting kilograms to pounds instantly.

Instead of building a heavy extension with unnecessary permissions and complex UI elements, I focused on:

Speed
Simplicity
Minimal design
Fast calculations
Easy accessibility
Why I Built It

Many conversion tools online are overloaded with ads, slow scripts, or complicated interfaces. I wanted something that opens quickly and provides instant conversion results without distractions.

Tech Stack
HTML
CSS
JavaScript
Firefox Extension APIs
Manifest V3
Features
Instant Kg ↔ Lbs conversion
Lightweight popup UI
Fast loading
Beginner-friendly interface
Responsive design
Minimal permissions
Challenges I Faced

One of the biggest challenges was keeping the extension lightweight while still making the UI modern and user-friendly.

I also spent time optimizing:

popup performance
layout responsiveness
extension packaging
cross-browser compatibility
What I Learned

Building browser extensions taught me a lot about:

UI simplicity
browser APIs
performance optimization
extension store policies
user-focused design
Future Improvements

I’m currently exploring:

better accessibility
offline support
history saving
multi-unit conversion support

Firefox Extension:


KG to LBS Converter & PDF Generator – Get this Extension for 🦊 Firefox (en-US)

Download KG to LBS Converter & PDF Generator for Firefox. A fast KG to LBS Firefox sidebar converter with chatbot, history, install page and uninstall feedback.

favicon
addons.mozilla.org

I’d love feedback and suggestions from the developer community.

Turn Your File System Into a SQL Database

Stop grepping. Start querying.

The 2-Minute Version:

# Install and scan your Documents folder
brew tap surveilr/tap && brew install surveilr
surveilr admin init -d fs.db
surveilr ingest files -r ~/Documents -d fs.db

# Query it
surveilr shell -d fs.db
-- Find every PDF modified this month
SELECT file_path, size_bytes, last_modified
FROM files
WHERE extension = 'pdf' AND last_modified > date('now', 'start of month');

That’s it. Your filesystem is now a SQLite database you can query forever.

The Problem with grep and find

You’ve been there:

find ~/Documents -name "*.pdf" -mtime -30 | xargs ls -lh

It works. Until you need to:

  • Track changes over time
  • Join file metadata with other data
  • Build complex filters
  • Share results in a structured format
  • Query the same data six months later

Then your one-liner becomes a 200-line Bash script nobody can maintain.

Here’s a different approach:

Turn your filesystem into a permanent SQLite database you can keep querying with standard SQL.

No scripts. No parsing. Just tables.

What surveilr Does

surveilr scans directories and captures everything into SQLite:

  • File paths and names
  • Sizes and timestamps
  • Content hashes (SHA-256)
  • Extensions and MIME types
  • Permissions and ownership

The output is a standard .db file you can:

  • Query with any SQLite tool
  • Open in DB Browser, DBeaver, DataGrip
  • Read with Python pandas or R
  • Keep forever (SQLite files last decades)
  • Share with teammates (just copy the file)

Step 1: Create a Database

surveilr admin init -d my-files.db

This creates an empty SQLite database. That’s it. No configuration, no setup, no servers.

You can open this file in any SQLite client right now and see the schema.

Step 2: Scan a Directory

surveilr ingest files -r ~/Documents -d my-files.db

surveilr walks the directory tree and inserts metadata into two tables:

  • uniform_resource — Core metadata (hash, size, timestamps)
  • ur_ingest_session_fs_path_entry — File-specific data (paths, extensions)

Pro tip: Run multiple times to track changes over time. Each scan creates a new session.

Step 3: Query Like You Mean It

Open the SQL shell:

surveilr shell -d my-files.db

Now you can run queries that would be painful in Bash.

Find all PDFs larger than 5MB

SELECT
    e.file_path_abs,
    e.file_basename,
    u.size_bytes / 1024 / 1024 AS size_mb
FROM uniform_resource u
JOIN ur_ingest_session_fs_path_entry e
  ON u.uniform_resource_id = e.uniform_resource_id
WHERE e.file_extn = 'pdf'
  AND u.size_bytes > 5242880
ORDER BY u.size_bytes DESC;

Track file modifications over time

Run multiple scans, then query across sessions:

SELECT
    e.file_basename,
    COUNT(DISTINCT u.content_digest) AS versions,
    MIN(u.last_modified_at) AS first_seen,
    MAX(u.last_modified_at) AS last_modified
FROM uniform_resource u
JOIN ur_ingest_session_fs_path_entry e
  ON u.uniform_resource_id = e.uniform_resource_id
WHERE e.file_basename = 'report.docx'
GROUP BY e.file_basename;

Find duplicate files by content hash

SELECT
    u.content_digest,
    COUNT(*) AS copies,
    SUM(u.size_bytes) / 1024 / 1024 AS total_mb_wasted
FROM uniform_resource u
JOIN ur_ingest_session_fs_path_entry e
  ON u.uniform_resource_id = e.uniform_resource_id
GROUP BY u.content_digest
HAVING copies > 1
ORDER BY total_mb_wasted DESC;

Find files that changed in the last 7 days

SELECT
    e.file_path_abs,
    u.last_modified_at,
    u.size_bytes
FROM uniform_resource u
JOIN ur_ingest_session_fs_path_entry e
  ON u.uniform_resource_id = e.uniform_resource_id
WHERE u.last_modified_at > datetime('now', '-7 days')
ORDER BY u.last_modified_at DESC;

These queries work on any SQLite database—forever.

Transform Structured Files Into Queryable Tables

surveilr doesn’t just capture file metadata—it can extract and transform content from structured files.

This is where it gets interesting.

Transform CSV Files Into SQL Tables

Got CSV files? Turn them into queryable tables automatically:

surveilr orchestrate transform-csv -d my-files.db

Every CSV file you ingested becomes a SQL table. If you scanned a directory with sales-2024.csv, you can now query it:

SELECT * FROM sales_2024 WHERE revenue > 10000;

No manual schema definition. No import scripts. Just automatic table creation.

Extract Data from HTML Files with CSS Selectors

Ingested HTML files? Extract specific elements with CSS selectors:

# Extract all paragraphs inside divs
surveilr orchestrate transform-html -d my-files.db 
  --css-select "paragraphs:div > p"

# Extract all links
surveilr orchestrate transform-html -d my-files.db 
  --css-select "links:a[href]"

# Extract headers
surveilr orchestrate transform-html -d my-files.db 
  --css-select "headers:h1, h2, h3"

The extracted data goes into JSON format you can query:

SELECT
    file_path,
    json_extract(transformed_content, '$.paragraphs') AS paragraphs
FROM uniform_resource
WHERE file_extn = 'html';

Real-world use case: Scrape product data from saved HTML pages, extract structured content from documentation, parse scraped web pages.

Transform Markdown Files

Extract specific sections from Markdown files:

# Extract all headers
surveilr orchestrate transform-markdown -d my-files.db 
  --md-select "headers:# *"

# Extract code blocks
surveilr orchestrate transform-markdown -d my-files.db 
  --md-select "code:```
{% endraw %}
*
{% raw %}
```"

Real-world use case: Index documentation, extract code examples from README files, parse technical notes.

Transform XML Files

XML files get automatically parsed into queryable JSON:

surveilr orchestrate transform-xml -d my-files.db

Then query XML content with SQLite’s JSON functions:

SELECT
    file_path,
    json_extract(transformed_content, '$.root.item[0].name') AS item_name
FROM uniform_resource
WHERE file_extn = 'xml';

Real-world use case: Parse configuration files, process exported data from tools, analyze XML logs.

Why This Matters

Most file indexing tools just capture metadata.

surveilr extracts and transforms content so you can query inside your files.

This means you can:

  • Query values within CSV files, not just the file existence
  • Search for specific HTML elements across hundreds of pages
  • Extract code snippets from all Markdown docs
  • Parse XML configs and query their structure

Example workflow:

# 1. Ingest a directory of mixed files
surveilr ingest files -r ~/project-docs -d docs.db

# 2. Transform CSVs into tables
surveilr orchestrate transform-csv -d docs.db

# 3. Extract headers from Markdown
surveilr orchestrate transform-markdown -d docs.db --md-select "headers:# *"

# 4. Query across file types
surveilr shell -d docs.db
-- Find all CSV files with revenue data
SELECT * FROM sales_data WHERE revenue > 50000;

-- Find all Markdown headers mentioning "API"
SELECT file_path, json_extract(transformed_content, '$.headers')
FROM uniform_resource
WHERE file_extn = 'md'
  AND json_extract(transformed_content, '$.headers') LIKE '%API%';

You just turned a directory of mixed files into a queryable knowledge base.

Watch Mode: Continuous Indexing

Want surveilr to automatically re-index when files change?

surveilr ingest files -r ~/Documents -d my-files.db --watch

surveilr monitors the directory and updates the database in real-time.

Press Ctrl+C to stop.

Export Results

Need to share findings? Export as JSON, CSV, or Markdown:

surveilr shell -d my-files.db --cmd "
SELECT file_path, size_bytes, last_modified
FROM files
WHERE extension = 'pdf'
" --output json > pdfs.json

Or save queries as reusable SQL files:

cat > find-large-files.sql <<'EOF'
SELECT file_path, size_bytes / 1024 / 1024 AS size_mb
FROM files
WHERE size_bytes > 10485760
ORDER BY size_bytes DESC
LIMIT 20;
EOF

surveilr shell -d my-files.db find-large-files.sql

Why Not Just Use Scripts?

You could write a Python script to walk directories and output JSON.

But:

Scripts have no schema.
Next month, you won’t remember what fields you captured or what format you used.

surveilr creates stable SQL tables.
The schema is self-documenting.

Scripts don’t track history.
You get a point-in-time snapshot, then it’s gone.

surveilr accumulates sessions over time.
Run it weekly and query across months of history.

Scripts don’t compose.
How do you join your file scan with your email data?

surveilr stores everything in one database.
Cross-domain joins just work.

Forensic Investigation Workflows

Once your filesystem is queryable, you can ask surprising questions:

Find files that disappeared

-- Files that existed in session 1 but not session 2
SELECT file_path
FROM files WHERE session_id = 'session_1'
EXCEPT
SELECT file_path
FROM files WHERE session_id = 'session_2';

Track when specific files were modified

SELECT file_basename, last_modified_at, size_bytes
FROM files
WHERE file_basename LIKE '%contract%'
ORDER BY last_modified_at DESC;

Find files created during a specific time window

-- Files created during a deployment window
SELECT file_path, last_modified
FROM files
WHERE last_modified BETWEEN '2024-05-10 14:00' AND '2024-05-10 15:00';

These queries are impossible with find and grep.

Real-World Uses

For Developers

  • Track which files changed before/after a bug appeared
  • Find orphaned build artifacts
  • Identify large files bloating repos
  • Build custom file search tools

For Operations

  • Audit log file growth over time
  • Track config file changes
  • Find stale backups
  • Monitor disk usage trends

For Security Teams

  • Establish filesystem baselines
  • Detect unauthorized file modifications
  • Track file access patterns
  • Identify anomalous file creation

For Compliance (Oh, By the Way)

  • Document where regulated data lives (HIPAA PHI, GDPR PII)
  • Prove file retention policies
  • Provide audit evidence
  • Respond to data access requests

But the real power is permanent queryability.

Open the Database in Other Tools

Because surveilr outputs standard SQLite, you can use any SQLite tool:

  • DB Browser for SQLite — Visual database inspector
  • DBeaver — Universal database client
  • Datasette — Instant web UI for your database
  • Python pandas — pd.read_sql_query(sql, 'sqlite:///my-files.db')
  • DuckDB — Fast analytical queries on SQLite files
  • VS Code SQLite extensions — Query in your editor

You own the .db file. Use whatever tools you want.

Troubleshooting

“Permission denied” errors?
Run with appropriate permissions or scan a different directory.

Database getting too large?
Use --dry-run to preview:

surveilr ingest files -r ~/Documents --dry-run

Want to see what happened?
Add --stats to show ingestion summary:

surveilr ingest files -r ~/Documents -d my-files.db --stats

What’s Next?

You just turned your filesystem into a queryable database.

Now combine it with other data sources:

Or explore more:

The Bottom Line

Your filesystem should be queryable, not just searchable.

find and grep are great for one-off questions.

But when you need:

  • Historical tracking
  • Complex queries
  • Structured output
  • Composability
  • Permanence

…you want SQL.

surveilr turns your filesystem into a SQLite database you own forever.

No scripts. No parsing. Just queries.

Ready to query your filesystem? Install surveilr and run your first scan.

Get Started →

How I Built a YouTube Shorts Brief Generator with PHP and the OpenAI API (in a Few Hours)

Tool Input Screenshoot

I’ve been building a YouTube Shorts automation engine as a side project — the kind of system that takes a niche, generates a script, hands it to a text-to-speech API, pairs it with stock footage, and uploads it automatically.

The pipeline itself is a bigger build. But one thing kept slowing me down at the start of every session: I’d sit down to test a new part of the system and realise I needed a decent script to feed into it. Writing one from scratch every time was killing momentum.

So I built a small tool to solve that problem. A few hours later I had something I actually wanted to use — and apparently other people did too.

Here’s how I built it.

What the tool does

You give it a niche and a topic. It gives you back a complete YouTube Shorts brief:

  • Hook — one punchy line, max 12 words, engineered to stop the scroll
  • Script — 45–60 seconds spoken, conversational, with a built-in insight and soft CTA
  • B-roll notes — 4 specific visual suggestions matched beat-for-beat to the script
  • Caption — under 150 characters, copy-paste ready
  • Hashtags — 8 tags, mix of high-volume and niche-specific

The whole thing runs on PHP, calls the OpenAI API server-side, and sits behind a Stripe credit system.

The stack

  • PHP 8.x (already on my shared hosting)
  • Bootstrap 5 for the frontend
  • OpenAI gpt-4o-mini — cheap, fast, good enough
  • Stripe Payment Links + webhooks
  • PHPMailer for transactional email
  • MySQL for user and credit storage

Nothing exotic. Deliberately so — I wanted something I could deploy to shared hosting without touching a terminal.

The prompt engineering

This is where most of the actual work happened. Getting the model to return clean, consistently structured output without preamble or markdown took more iteration than the code did.

The final system prompt:

You are an expert YouTube Shorts scriptwriter.

Your job is to generate a complete, ready-to-publish YouTube Shorts brief.

Return ONLY the following structure, with NO preamble, no commentary, no markdown formatting:

🪝 HOOK
[One punchy spoken line that stops the scroll. Use curiosity, shock, or a bold claim. Max 12 words.]

📝 SCRIPT
[Natural, conversational script for 45-60 seconds spoken. No jargon. Short sentences. 
Each line flows into the next. Include one surprising fact or insight. 
End with a soft CTA like "follow for more" or "comment below."]

🎬 B-ROLL
[4 specific visual suggestions that match the script beat by beat. Be precise — 
not "show a phone" but "close-up of ChatGPT interface generating a response in real time."]

📣 CAPTION
[One punchy caption under 150 characters with a hook and 2-3 relevant emojis.]

#️⃣ HASHTAGS
[8 hashtags — mix of high volume and niche specific. One per line. No spaces between words.]

The key things that made it work:

  1. “Return ONLY” — without this you get preamble every time
  2. Emoji section headers — made parsing reliable and consistent
  3. Negative examples in the B-roll instruction — telling it what not to do (“not ‘show a phone’ but…”) produced dramatically more specific output
  4. Word limits on the hook — “max 12 words” was enforced much better than “short”

The PHP backend

The frontend posts to generate.php, which handles the OpenAI call server-side so the API key never touches the browser:

$payload = json_encode([
    'model'      => 'gpt-4o-mini',
    'max_tokens' => 1000,
    'messages'   => [
        ['role' => 'system', 'content' => $system_prompt],
        ['role' => 'user',   'content' => "Niche: {$niche}nTopic: {$topic}nTone: {$tone}"],
    ]
]);

$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $api_key,
    ],
    CURLOPT_TIMEOUT => 30,
]);

Standard stuff. The response parsing just pulls $decoded['choices'][0]['message']['content'] and sends it back as JSON.

Parsing the output on the frontend

Since the model returns consistent emoji-headed sections, parsing is straightforward:

function extract(text, startMarker, endMarker) {
    const start = text.indexOf(startMarker);
    if (start === -1) return '';
    const afterMarker = text.indexOf('n', start) + 1;
    const end = endMarker ? text.indexOf(endMarker, afterMarker) : text.length;
    return text.slice(afterMarker, end === -1 ? text.length : end).trim();
}

const sections = {
    hook:     extract(text, '🪝 HOOK',     '📝 SCRIPT'),
    script:   extract(text, '📝 SCRIPT',   '🎬 B-ROLL'),
    broll:    extract(text, '🎬 B-ROLL',   '📣 CAPTION'),
    caption:  extract(text, '📣 CAPTION',  '#️⃣ HASHTAGS'),
    hashtags: extract(text, '#️⃣ HASHTAGS', null),
};

Each section renders into its own panel with a copy button.

The credit system

Initially I thought about selling lifetime access — £27 once, use it forever. Then I did the maths. gpt-4o-mini costs roughly £0.01 per generation. A heavy user could generate 1,000 briefs and cost me £10 on a one-time payment. Not sustainable.

Credit packs made more sense:

Pack Credits Price
Starter 30 £7
Creator 100 £19
Operator 300 £47

Credits never expire. Users get an email alert when they drop to 10 remaining. Top-ups add to their existing balance via the same access link — no new account, no friction.

The MySQL table is simple:

CREATE TABLE `users` (
  `token`           CHAR(64)      NOT NULL,
  `email`           VARCHAR(255)  NOT NULL,
  `credits`         INT UNSIGNED  NOT NULL DEFAULT 0,
  `credits_used`    INT UNSIGNED  NOT NULL DEFAULT 0,
  `low_credit_sent` TINYINT(1)    NOT NULL DEFAULT 0,
  UNIQUE KEY `token` (`token`),
  KEY `email` (`email`)
);

The Stripe webhook gotcha

Stripe occasionally fires duplicate webhook events — either retries or genuine duplicates. The first version of the webhook used payment_intent as the idempotency key, which didn’t catch all cases.

The fix was to use $session['id'] — the checkout session ID — which is always the same on retries, and back it up with a separate payments table with a unique constraint:

// Try to insert — unique constraint rejects duplicates
try {
    $pdo->prepare('
        INSERT INTO payments (payment_intent, email, pack, credits_added)
        VALUES (?, ?, ?, ?)
    ')->execute([$session_id, $buyer_email, $pack_key, $credits_to_add]);
} catch (PDOException $e) {
    // Already processed — return 200 silently
    http_response_code(200);
    exit;
}

The whole credit update runs inside a transaction. If two requests race past the initial check simultaneously
, only one can insert into payments. The other hits the unique constraint and exits without touching credits.

What I’d do differently

Use a queue for webhook processing. On shared hosting, if the webhook takes more than a few seconds Stripe retries it. A job queue would let you return 200 immediately and process in the background — cleaner than relying purely on idempotency keys.

Add usage analytics from day one. I can see total credits used per user but I have no visibility into which niches or topics are generating the most briefs. That data would be useful for marketing.

Test the email earlier. PHP’s mail() is unreliable on shared hosting. I switched to PHPMailer with SMTP on day two — should have started there.

The result

Tool Input Screenshoot

Tool Output SCreenshot

The tool is live at https://creator.buizy.io

If you’re building a faceless YouTube channel or a Shorts automation pipeline and want to stop writing briefs by hand, it’s £7 to try.

If you’re a developer building something similar, hopefully the prompt structure and idempotency approach save you a few hours of head-scratching.

Happy to answer questions in the comments.

Built with PHP, OpenAI, Stripe, and too much coffee.

Turn Anything Into a Queryable SQLite Database

grep, jq, ETL, and forensic indexing collapsed into one local SQL primitive

Your audit trail should be a database you own, not a SaaS UI you rent.

surveilr turns your files, emails, and APIs into standard SQLite databases you can query forever—with any tool, offline, on your machine.

No cloud. No dashboards. Just SQL.

See It Work in 2 Minutes

Install surveilr and immediately query your filesystem:

# Install (macOS/Linux)
brew tap surveilr/tap && brew install surveilr

# Create a database and scan your Documents folder
surveilr admin init -d my-data.db
surveilr ingest files -r ~/Documents -d my-data.db

# Open SQL shell and query
surveilr shell -d my-data.db

Now run surprisingly powerful queries:

-- Find every PDF modified after a specific date
SELECT file_path, last_modified, size_bytes
FROM files
WHERE extension = 'pdf'
  AND last_modified > '2024-05-01';

-- Track file changes over time
SELECT file_basename, COUNT(*) as versions
FROM files
GROUP BY file_basename
HAVING versions > 1;

-- Find orphaned large files
SELECT file_path, size_bytes / 1024 / 1024 AS size_mb
FROM files
WHERE size_bytes > 10485760
ORDER BY size_bytes DESC;

That’s it. You just turned your filesystem into a queryable database you can keep investigating.

What is surveilr?

surveilr is a local-first universal SQL layer that ingests operational data and outputs standard SQLite databases.

It’s not a platform. It’s not a dashboard. It’s an ingestion layer that speaks SQL.

Core Capabilities

  • 📂 File system indexing — Turn any directory into queryable metadata
  • 🔄 Content transformation — CSV→SQL tables, HTML→JSON (CSS selectors), Markdown/XML→queryable data
  • 📧 Email ingestion — IMAP to SQLite (Gmail, Outlook, any server)
  • 🔌 API extraction — 600+ Singer taps (GitHub, Jira, Salesforce, databases)
  • 🔍 Standard SQL — No custom DSL, just SQLite
  • 🏠 Local-first — Everything runs on your machine, offline-capable
  • 🔒 You own the data — Portable .db files you control forever

Why This Matters

Most operational data ends up in one of three dead ends:

  1. SaaS dashboards you can’t query your way
  2. One-off scripts that become unmaintainable
  3. CSV exports that lose context over time

surveilr gives you a different option: permanent, queryable SQLite databases.

Years from now, you’ll still be able to open that .db file with any SQLite client and run new queries you haven’t thought of yet.

The SQLite Advantage

surveilr doesn’t invent a new database format. It uses SQLite—the world’s most deployed database.

You already trust SQLite. It’s in your phone, your browser, your laptop’s OS.

What surveilr adds is disciplined ingestion patterns that turn messy operational data into clean, queryable tables.

Inspectable: Open any .db file with DB Browser, VS Code, or sqlite3
Durable: SQLite files last decades
Interoper able: Works with Datasette, DuckDB, pandas, Observable, Grafana
Portable: One file, zero dependencies
Permanent: Your data doesn’t disappear when a vendor shuts down

This is the opposite of SaaS data lock-in.

Queries That Make You Go “Wait, I Can Do THAT?”

The power comes from cross-domain queries you can’t run anywhere else.

Find documents mentioned in emails after they were modified

SELECT f.file_path, e.subject, e.date, f.last_modified
FROM files f
JOIN emails e ON e.subject LIKE '%' || f.file_basename || '%'
WHERE f.last_modified < e.date
ORDER BY e.date DESC;

Track all GitHub commits made within 24 hours of a production incident

SELECT c.commit_sha, c.author, c.message, c.timestamp
FROM github_commits c
WHERE c.timestamp BETWEEN
  (SELECT incident_time - interval '24 hours' FROM incidents WHERE id = 'INC-123')
  AND
  (SELECT incident_time FROM incidents WHERE id = 'INC-123');

Find invoices discussed in email without matching purchase orders in Jira

SELECT e.subject, e.from, e.date
FROM emails e
WHERE e.subject LIKE '%invoice%'
  AND NOT EXISTS (
    SELECT 1 FROM jira_issues j
    WHERE j.summary LIKE '%' || SUBSTR(e.subject, INSTR(e.subject, 'INV-'), 10) || '%'
  );

These aren’t theoretical examples. These are real forensic workflows you can build.

Three Practical Guides

Guide 1: Query Your File System

Read the full guide →

Scan directories and query file metadata with SQL. Plus: Transform CSVs into SQL tables, extract data from HTML with CSS selectors, parse Markdown and XML into queryable JSON.

2-minute win: Find all PDFs modified in the last 30 days
Bonus: Turn CSV files into queryable SQL tables automatically

Guide 2: Turn Email Into SQL

Read the full guide →

Ingest Gmail/Outlook via IMAP. Query conversations, track threads, extract attachments, search across years of email history.

2-minute win: Find all emails from a specific sender mentioning “invoice”

Guide 3: Extract API Data

Read the full guide →

Use Singer taps to pull data from GitHub, Jira, GitLab, Salesforce, or 600+ other sources. Join across platforms.

2-minute win: Query all GitHub commits from the last 7 days

Why Not Just Write a Script?

You could. But:

Scripts become dead ends.
Six months later, you can’t remember what format you used or where you saved the output.

surveilr outputs permanent, standard SQLite databases.
Open them years later with any SQL tool and keep querying.

Scripts don’t compose.
You can’t easily join your email script’s output with your file scan script’s output.

surveilr stores everything in one queryable database.
Cross-domain joins just work.

Scripts have no schema.
You’re parsing JSON with jq and hoping the structure doesn’t change.

surveilr normalizes data into stable SQL tables.
Query with confidence.

Ecosystem Integration

surveilr isn’t the destination. It’s the ingestion layer.

Once your data is in SQLite, you can use any tool in the SQLite ecosystem:

You own the database. Use whatever tools you want.

Local-First. No Cloud. You Own It.

This is a big deal in 2026.

Most “compliance platforms” hide your own data behind proprietary dashboards.

surveilr gives you raw SQL access to everything.

  • ✅ All data stays on your machine — No upload, no sync, no cloud dependency
  • ✅ Works completely offline — Internet not required
  • ✅ Inspectable — Open the .db file with any SQLite client
  • ✅ Portable — Copy the file, query it anywhere
  • ✅ No vendor lock-in — Standard SQLite format, not proprietary
  • ✅ Privacy-first — Your data never leaves your control

If you’re tired of SaaS platforms that:

  • Charge per seat
  • Lock your data behind APIs
  • Disappear when the startup shuts down
  • Require constant internet connectivity

…then surveilr is for you.

Oh, By the Way: Compliance Teams Love This Too

If you work in healthcare, finance, or regulated industries, surveilr happens to be perfect for:

  • HIPAA audits — Track where PHI files are stored
  • SOX compliance — Maintain 7-year email records with queryable evidence
  • GDPR requests — Respond to “right to access” with SQL queries
  • SOC 2 audits — Show complete change management history

But that’s a side effect of the real value: permanent, queryable operational data you control.

Forensic Curiosity

Once you start using surveilr, you’ll find yourself asking new questions:

  • What files were deleted but are still referenced in emails?
  • Which documents were modified right before a deployment?
  • What code changes correlate with customer support tickets?
  • Which team members touched files in a specific directory over the last year?
  • What attachments were sent externally that match internal file hashes?

These questions are impossible to answer with dashboards.

But with SQL, they’re just queries.

Installation

macOS / Linux

brew tap surveilr/tap && brew install surveilr

Verify

surveilr --version
surveilr doctor  # Check environment

For other platforms, see the installation guide.

Run This Now

Pick your 2-minute win:

Option 1: Query your filesystem

surveilr admin init -d fs.db
surveilr ingest files -r ~/Documents -d fs.db

Option 2: Query your email

surveilr admin init -d email.db
surveilr ingest imap -u [email protected] -p "app-password" -a imap.gmail.com -d email.db

Option 3: Query GitHub (Ingest Singer taps)

surveilr admin init -d github.db
surveilr ingest files -r ./github-tap-script.py -d github.db
surveilr orchestrate adapt-singer (convert data to views)

Then open the .db file in any SQLite tool and start exploring.

Learn More

The Bottom Line

Your operational data—files, emails, API responses—should be:

  1. Queryable (SQL, not grep)
  2. Permanent (SQLite, not CSVs)
  3. Yours (local, not SaaS)
  4. Composable (join across domains)
  5. Inspectable (open in any tool)

That’s what surveilr gives you.

It’s not compliance software. It’s not enterprise governance.

It’s grep, jq, ETL, and forensic indexing collapsed into one local SQL primitive you can build on forever.

Ready to own your data? Install surveilr and query something surprising.

Get Started →