Pages

Tuesday, May 12, 2026

Why I Treat Ad-Hoc Scripts Like Real Software

By Vicente Arteaga Gomez

MisLinux · Last updated: May 5, 2026

This is part of my Kubernetes-on-Hetzner-and-operations series on MisLinux. It comes from repeated operator work, not from a vendor playbook.

Ad-hoc scripts cover image

I used to use the phrase "just a quick script" as if it reduced the risk.

It did not.

The script still:

  • touched real data
  • encoded assumptions
  • created artifacts someone would rely on later
  • became copy-paste material for the next incident

At some point I stopped calling these scripts disposable. I started treating them as source code that merely had a short original deadline.

The rule I use now

If a script is important enough to run once against real operational data, it is important enough to deserve:

Minimum standardWhy it matters
DRY helpersavoid re-deriving parsing and transport logic later
testscatch the obvious contract breaks before the next rerun
CIR notespreserve why the workflow exists in this shape
saved artifactsprove what happened on that run

That does not mean every script needs a huge framework. It means the script should stop pretending it is exempt from engineering standards.

What changed my mind

The real problem was not the first run. The first run usually works well enough.

The problem was the second and third run:

  • slightly different data
  • slightly different date window
  • slightly different production state
  • slightly different operator

That is where "ad hoc" turns into "mysterious legacy."

A script can be short and still be real code

I do not need a large package structure before I apply discipline.

Even a small automation helper can be structured like this:

script entrypoint
  -> parse arguments
  -> call a shared library/helper
  -> write artifacts
  -> exit non-zero on real failure

That shape makes testing possible and keeps the second script from re-implementing the same logic a week later.

A concrete command trail I prefer

# Good: deterministic rerun with explicit output folder
php operations/example/report.php \
  --input artifacts/source.json \
  --output history/20260505-example-run/

# Good: test the shared logic separately
vendor/bin/phpunit tests/example/ReportBuilderTest.php

# Better: use the exact same helper from the next automation layer
php operations/example/next-stage-proof.php --from history/20260505-example-run/

This is much safer than three unrelated shell fragments that happen to work today.

The subtle benefit: better failure messages

Once I treat the script like real software, I also stop accepting vague operator output.

I want errors that explain:

  • what input was missing
  • what upstream state was unexpected
  • which artifact path contains the evidence

That matters more than people think. A one-off script with useless errors becomes a slow manual process again the moment it fails.

Failure case: the fake one-off

The fake one-off script usually looks like this:

  • one file
  • mixed fetching, parsing, mutation, and reporting
  • hardcoded ids or paths
  • no test seam
  • no artifact contract

Then somebody asks for the same run with a new date range or a new target, and now it has to be reverse-engineered from scratch.

Why this matters more with AI-assisted workflows

AI agents are extremely good at making a one-off script "work."

They are much less useful if the script:

  • hides its assumptions
  • cannot be re-run deterministically
  • has no tests
  • has no saved evidence

That is why I now want the script to be reusable before I want it to be clever.

My current checklist for a so-called ad-hoc script

Before I accept it, I want:

  1. a small shared library if parsing or transport logic may recur
  2. at least one unit test around the risky part
  3. an output directory or artifact contract
  4. one CIR note explaining why the script exists and what would go wrong if someone "simplified" it blindly

If it touches production directly, I want even more proof than that.

What I'd do differently now

I used to optimize for speed on the first run and structure on the second run. What I would do differently now is merge those decisions: build the smallest reusable version first. It is usually only slightly slower at the beginning and much faster after that.