Guardrails: What Scripts Must Never Do, and Who Checks
Thursday, 11:20, at Meridian Parts. Hana has been asked to look over a colleague's script before it becomes a button. She is reading it the way she reads everything on a Thursday, which is quickly. It clears a stuck profile cache for the warehouse scanners' sign-in, a fix the desk does twenty times a month. It works. Yusuf has tested it on his own machine and on five others and has the log to show it. On line fourteen there is the service account's password, in plain text, so the script can connect to the scanners' management share. On line twenty-two there is a command that removes a folder. The folder's name is built from a variable that is set three lines earlier from the scanner's reply. Nothing checks whether the variable is empty. Hana stops reading quickly. The script works. It is also two accidents waiting for a slow afternoon.
A guardrail is a rule about what a scripted fix may never do, or must always have, that is checked before the script is allowed to run rather than hoped for afterwards. The script in front of Hana passes every functional test and fails two guardrails. The point of guardrails is that passing the tests does not matter. Scripts do what they are told at machine speed, in many places, with nobody watching the middle. The mistakes they make are therefore the same mistake many times before anyone can say stop. The rails are how a desk makes the worst mistakes impossible rather than merely unlikely. This article is part of our Scripted Fixes & One-Click Remediations series. It is the one about the things the other five take for granted.
It covers why rails beat judgement for scripts; and the rule that nothing is deleted. It covers the rule that no credential lives in the script; and the rule that a run touches one machine. It covers the blast radius question and the account the script runs as. It covers the rule that it stops and says why; and the review that checks all of it. It ends with a short war story and the guardrail list to copy, in that order.
Why Rails, Not Judgement
A technician doing a fix by hand has judgement at every step, and a script has none. So the judgement has to be moved from the moment of running to the moment of writing. When Yusuf clears a scanner's cache by hand, he looks at the folder name before he removes anything. If the name looks wrong he stops without ever having decided to. The script does not look. It removes whatever the variable says, including nothing, which on some systems means everything in the current location. The rail "nothing is deleted" is that half-second of looking, written down once, and applied to every script forever.
Rails are also cheap in a way judgement is not. Checking a script against six rules takes a reviewer fifteen minutes and needs no knowledge of the fix itself. Checking whether the author exercised good judgement at every line takes an expert an hour and is still a guess. The rails are deliberately blunt. They forbid whole categories of action, including some that would occasionally have been fine. That is because the cost of the occasional loss is far smaller than the cost of the one time the exception goes wrong on forty machines. A desk that argues about each rail for each script has turned the rails back into judgement. The rails are the things you do not argue about.
And rails make the review possible for someone junior, which is how they scale. Hana does not need to understand the scanners' sign-in to see a password on line fourteen or a removal on line twenty-two. She needs the list, and the list fits on one page. A desk of three where any technician can review any script against a fixed list has more review capacity than a desk of thirty where only the senior engineer is trusted to read scripts. And the senior engineer is on leave.
Rail One: Nothing Is Deleted
A scripted fix does not delete, remove, purge, empty or clear anything a person made. Not files in a user's folders, not mailboxes, not accounts, not shares, not folders whose names come from a variable. The fix that genuinely requires deleting something is not a fix; it is a change. It gets a change record, a named person doing it by hand with their eyes open, and a rollback. That process is set out for small teams in What Needs Review and What Does Not. The rule sounds extreme until you remember that the whole point of a script is that nobody is watching it. A deletion is the one action for which "undo" is somebody's week.
The single exception is a cache the system rebuilds on its own. The exception is named, one cache per script, by its exact location, in the script's header. An application's rebuildable cache, a name-resolution cache, a stored sign-in token that the user will recreate by signing in again: these are things the machine made and will make again. Clearing them is the fix in many of the ten repeat fixes. The rail is not "never clear a cache". It is "never clear anything you have not named in advance, and never build the name from something the machine told you at run time". Yusuf's script failed the second half: the folder name came from the scanner's reply. A scanner that replied with nothing would have aimed the removal somewhere else entirely.
Disk clean-up, the fix every desk wants to script first, is not a scripted fix under this rule, and that is on purpose. Clearing temporary files, old downloads and forgotten installers is valuable and it is deletion. It belongs in a sweep with its own thresholds, its own list of exactly which locations may be touched, and a notice to the user. That sweep has its own article in Disk-Space Sweeps. Keep it out of the fix scripts, because a desk that has one script allowed to delete will soon have a reviewer who is used to seeing deletion in scripts. That reviewer is the rail's weakest point.
Rail Two: No Credentials in the Script
No password, key, token or connection string appears in the text of a script, ever, including the comments and including "just for testing". The reason is not that the script folder is insecure, although it may be. The reason is that scripts travel. They are copied to a laptop to debug and pasted into a chat to ask a question. They are attached to a ticket as "here is what we ran" and printed into a log when they fail. They are e-mailed to a successor with the words "this is how it all works". A secret in a script is a secret in every place the script has ever been, and nobody can list those places. The war story below is about exactly that, and the clean-up cost a weekend.
The script runs as an account, and the account's credentials are given to it by the thing that launches it, not by the text. A scheduled task runs under an account whose password is held by the operating system. A management tool runs its stored actions under an account it holds in its own vault. A wrapper script asks the system's secure credential store for the secret at run time, by name, and never writes it anywhere. Every operating system has such a store, and every management tool has a vault. The way to use them is in their documentation rather than here, because the mechanism changes and the rule does not. If the script needs to connect to something as someone, the someone is supplied from outside the script, every time.
Enforce the rail by searching, monthly, and make the search boring. Search every file in the scripts folder for the words "password", "pwd", "secret", "token" and "key". Also search for the pattern of your own domain's account names followed by a colon or an equals sign. Read every hit. A monthly search takes five minutes and finds the "temporary" credential that was going to be removed after testing, which is the only kind of credential that ever gets into a script. Nobody sets out to leave a password on line fourteen. They set out to come back to it.
Rail Three: One Machine Per Run
A scripted fix acts on exactly one machine, named on the way in. The name is checked against a list before anything runs. No lists of machines inside the script, no loops, no wildcards, no "all workstations in the warehouse", no free-text target that takes whatever is typed. The loop, where a loop is needed, lives outside the script in a person's hands, with a count. The person reads the log between batches. This feels like a limit on the script's usefulness and is in fact the whole of its safety. A script that can only touch one machine can only be wrong about one machine at a time. That is a speed at which a human can still catch it.
The rule has a cousin in time: one machine per run, and one run at a time per machine. Two copies of the same fix running on the same machine at once, because two technicians pressed the same button on the same ticket, is a race the script was never designed for. Most scripts lose it quietly. A simple lock, such as a marker file created at the start and removed at the end, lets the second run exit with "already running" rather than compete. It is three lines of script and it closes a hole that is otherwise opened on the busiest morning of the year. That is when two people are most likely to grab the same ticket.
When the fix genuinely needs to run on many machines, the many-machine run is a change, not a fix, however safe the single run has proved. A change record names the batch, the order, the person running it, the stop condition and the rollback. The run itself happens in batches of ten or so with the log read between them. The fifteen-minute version of that review is in A Change Review That Takes Fifteen Minutes. It is fifteen minutes well spent before anything touches forty machines. The script stays single-target throughout. The change is the loop, held by a person.
The Blast Radius Question
The blast radius of a script is the worst that can happen if it goes wrong. The question is asked four ways before any script is released: wrong machine, wrong time, twice, and everywhere. What happens if it runs on a machine that did not need it? What happens if it runs while the user is in the middle of something? What happens if it runs twice in a row? And what would happen if, despite rail three, it somehow ran on every machine you manage? The fourth question is the one that finds the fix that should never have been a script, because the honest answer is sometimes "we would be on the news".
Answer each question in writing, in the script's header, in the user's terms, and let the answers set the script's permissions. A fix whose four answers are "nothing", "nothing", "nothing" and "a lot of unnecessary restarts" can be a button anyone presses. A fix whose answers include "unsaved work lost" is second-line only with a confirmation that says so. A fix whose answers include "users locked out", "data gone" or "service down for everyone" does not get written. In that case, the time saved is spent on the change record instead. The table below is the short form, with the kind of fix that lands in each row.
| Worst case if it ran everywhere at once | Example fix | What the desk allows |
|---|---|---|
| Nothing visible; background services restart where they did not need to | Remap a missing drive; resync a clock; re-apply policy; flush a name cache | A button anyone on the desk may press; a self-healing candidate later |
| Users briefly interrupted; a queue or session lost; work can be redone | Restart the print spooler; restart a remote-access client; clear a sign-in token | A button for second line, with a confirmation that names the cost |
| Unsaved work lost for whoever was working | End a stuck application process; sign the user out of a service | Owner-only button; user told first; never self-healing |
| Users locked out, data removed, or a shared service down for everyone | Anything that deletes, disables accounts, or acts on a server others depend on | Not a scripted fix; a change record and a person, by hand |
Where the worst case reaches a whole site or a shared service, you are no longer talking about a fix but about the thing that becomes a major incident if it misfires. The criteria for that are in What Makes an Incident Major. If a script's fourth answer would meet those criteria, that is the end of the conversation about scripting it. The blast radius question is the one rail that occasionally kills a good idea, and it is supposed to.
Remember: a scripted fix never deletes what a person made, and never contains a credential. It acts on one named machine per run and runs as an account with only the rights the fix needs. It stops and says why when anything is unexpected. It is read by a second person against this list before its first run and after every edit.
Rail Four: The Account It Runs As
The script runs under an account that can do the fix and nothing else. That account is never a domain administrator and never a technician's everyday account. A spooler restart needs the right to stop and start one service on workstations. A drive remap runs in the user's own context and needs nothing extra at all. A cache clear needs write access to one folder. Give the account those rights, on workstations only. Give it nothing on servers, nothing in the directory beyond reading, and no access to anyone's files. If the fix cannot be done with such an account, that is information about the fix, not a reason to reach for the account that can do everything.
The reason is the same as for every other rail: nobody is watching. A technician logged on as an administrator doing a fix by hand is a risk for the minutes it takes. A script running as an administrator is that risk every time it runs, on every machine, with any bug it contains. Yusuf's removal on line twenty-two would have been an inconvenience under a scanner-maintenance account with rights to one folder, and a catastrophe under the account the desk used "because it always works". The principle is called least privilege. The practice is a separate account per fix family, named for what it does, with a description that says which scripts use it.
The same rules that govern an unattended remote-access account govern a script's account, because both are a standing power to act on machines when nobody is sitting at them. The policy that bounds unattended access, with its logging and its review, is in Unattended Access Policy. Review the script accounts when you review that policy. An account that no script uses any more is removed. An account whose rights have grown because a script "needed one more thing" is split in two. Rights, like scripts, breed quietly if nobody counts them.
Rail Five: It Stops and Says Why
A script that finds anything other than what its procedure described stops, writes a line saying what it found, and exits with the code for "refused". It does not guess, it does not try something else, and it does not retry in a loop. The reasons are set out earlier in this series, in the article on turning a fix into a script. This article adds only that the rule is a rail, not a style preference, and the reviewer checks it. A script with a branch that reads "otherwise, try the other way" has a decision in it that no one wrote down. That branch runs precisely when the script is most confused about the machine it is on.
Stopping includes timeouts on every wait, and it includes failing loudly. A script that waits without a limit cannot be told apart from one that is working. A script that prints "done" before its final check has reported success that may not have happened. Both are failures of the rail. Both are found by reading the script's last ten lines, which is where the reviewer should spend half her time. The first ten lines say what the script intends. The last ten say what it admits.
Finally, the script says why in words a person can act on. "Refused: expected one profile folder, found none" names the next step. "Error" does not. The reviewer reads every refusal and failure message and asks whether a trainee at 16:55 would know what to do with it. If the answer is no, the message is rewritten before the script is released. That is a small thing to insist on. It is also the thing that turns every refusal into a lesson about the estate rather than a mystery.
The Review: A Second Pair of Eyes
Every script is read by someone other than its author against the guardrail list before its first real run, and again after every edit. The reader's name and the date go in the header. The review is not a code review in the sense a software team would mean. It does not care about style, naming or elegance. It cares whether the six rails hold, and it takes a quarter of an hour. The reviewer reads the header for the blast radius answers and the named cache. They search the text for credentials, look for any removal and check where its target comes from, and confirm the target machine is a single checked input. They read the account the script runs as, and read the last ten lines for the stop behaviour. Then they sign.
The reviewer may be junior, and often should be. A trainee who has read this article can check every rail, because the rails are written so that they need no knowledge of the fix. More than that, the trainee reads the script as the stranger who will one day have to run it. A script that a trainee cannot follow is a script that will be run blind. Hana was not an expert on the warehouse scanners. She did not need to be. She needed line fourteen and line twenty-two, and she found them because she had a list that told her what to look for.
The review also protects the author, which is the part worth saying out loud on a small desk where review can feel like distrust. A script that has been read by a second person and signed is a script whose author is not alone when it goes wrong. "Yusuf's script broke the scanners" and "the script we reviewed and released broke the scanners" are different sentences. The second one is the one a healthy desk says. The review is where a mistake becomes the desk's instead of one person's, which is where mistakes belong.
A Short War Story: The Password on Line Fourteen at Kestrel Print
Kestrel Print, a commercial printer with about sixty workstations, had a script that re-registered the big production printers after the print server was patched. The script held the print server's service account password in plain text so it could connect without prompting. A technician named Callum, helping a colleague over chat, pasted the whole script into the conversation. Then, trying to be thorough, Callum pasted the same text into the ticket as the record of what had been run. The ticket's note was a public reply rather than an internal one. So the requester in the pre-press department had the password in his inbox within a minute. Nobody misused it, as far as anyone knows, which is not a sentence an auditor accepts. The account's password had to be changed. Because the same account was used by three other scripts and two scheduled tasks on different machines, the change took a Saturday and broke two of them until Monday. The rewritten script asked the operating system's credential store for the secret by name. The account was split into one per task. The desk's guardrail list acquired its first line. Callum's note at the bottom of the incident record said that a secret in a script is a secret everywhere the script has ever been. That is a sentence the desk now puts in the header of every script it writes.
The pasting error in that story is the easy half to prevent. If you run QuerySolv, the distinction between a public reply and an internal note is built into every ticket. A script's output, logs and text belong in the second, always. In any tool, the rule is the same and the review checks that the button's record goes to the internal side. The hard half is the password on line fourteen, and only the rail prevents that.
The Guardrail List
The list below is the review, and it is also the policy. Print it, pin it where the scripts are written, and read every script against it before the first run and after every edit. It is deliberately short, because a list the reviewer can hold in their head is a list that gets used. A six-page scripting policy is a document that is read once at induction and then stored safely. Each line is a yes-or-no. A "no" on any line in the first five sections stops the release. A "no" in the review section means the review has not happened yet.
GUARDRAIL LIST — EVERY SCRIPTED FIX, BEFORE FIRST RUN AND AFTER EVERY EDIT
Script: ______________________ Author: ______________ Reviewer: ______________
Date reviewed: ________ Edit reviewed (if not first run): ________________________
1. NOTHING IS DELETED
[ ] No command removes, purges, empties or clears anything a person made
[ ] The only cleared item is a rebuildable cache, named by exact location in the header
[ ] No target of any removal is built from a value read at run time
[ ] Disk clean-up is NOT in this script (it belongs to the sweep, with its own rules)
2. NO CREDENTIAL IN THE SCRIPT
[ ] No password, key, token or connection string in the text, comments included
[ ] The account's credentials come from the task scheduler, the tool's vault,
or the operating system's credential store, by name, at run time
[ ] The scripts folder was searched this month for: password, pwd, secret, token, key
3. ONE MACHINE PER RUN
[ ] The target is one machine name, given as input, checked against the managed list
[ ] No list, loop, wildcard, "all", or free-text target anywhere in the script
[ ] A lock prevents two runs on the same machine at once
[ ] Any many-machine run is a change record, batched by a person, log read between batches
4. BLAST RADIUS ANSWERED (in the header, in the user's words)
[ ] Wrong machine: ___________________________________________________
[ ] Wrong time (user mid-task): ______________________________________
[ ] Twice in a row: _________________________________________________
[ ] Everywhere at once: _____________________________________________
[ ] Permission set from the worst of the four (anyone / second line / owner / not a script)
5. RUNS AS THE RIGHT ACCOUNT
[ ] Named account for this fix family; not a domain administrator; not a person's account
[ ] Rights: only what the fix needs, on workstations only
[ ] No rights on servers, no write access to the directory, no access to user files
[ ] Account listed in the unattended-access review
6. STOPS AND SAYS WHY
[ ] Unexpected state exits "refused" with a reason a trainee could act on
[ ] Every wait has a time limit and logs a failure when it runs out
[ ] No retry loops; no "otherwise try the other way"
[ ] Nothing prints "done" or "fixed" before the check after the action
7. REVIEWED
[ ] Read in full by someone other than the author, against this list
[ ] Reviewer's name and date in the script header
[ ] Re-read after every edit, however small
[ ] Release blocked on any "no" in sections 1 to 6
Release approved: Y / N Signed: ______________ Date: ________
Yusuf's script went back to him with two lines marked. It came back the next morning with the password gone. The removal had been replaced by a clear of one named cache folder whose path was fixed in the header. There was also a refusal if the scanner's reply was empty. Hana signed it on the Friday. It has been a button for the warehouse team since, pressed about twenty times a month. It has refused four times, each because a scanner answered with nothing. That is exactly the case that would once have aimed a removal at the wrong place. The rails did not make the script slower. They made it the script it should have been on Thursday.
Frequently Asked Questions
Our most useful fix clears old files to free disk space. Is it really banned?
The script needs to connect to a server as a service account. Where does the password go?
Who should review scripts on a desk of two?
Can a script ever act on a server rather than a workstation?
How often should the guardrail list itself be reviewed?
What do we do with a script that already breaks a rail and is in daily use?
From the QuerySolv team: we build self-hosted help desk software for Windows. QuerySolv runs as one Windows service on your own network. It takes requests by web form, e-mail, live chat, social messages and REST API. It is licensed once per installation rather than per agent per month. The free trial runs the full product for 30 days.
