Generating Shell Commands With LLMs in October 2026: Explain Before You Run, Dry-Run Destructive Commands, and Never Pipe Model Output Straight Into Bash
Asking a language model for a shell command is one of the fastest ways to get unstuck. You describe what you want, such as "find every log file over 100 MB that hasn't changed in a month and compress it," and a one-liner comes back in seconds. That speed is also the risk. A shell command runs with your permissions, against your real files, and many of the mistakes a model can make are not reversible. A wrong glob, a missing quote, or a flag that means something different on macOS than on Linux can delete, overwrite, or leak data before you notice.
The answer is not to stop using AI for shell work. It is to put a small, consistent routine between the model's output and your terminal. This guide covers that routine: how to ask, how to read what comes back, how to test it safely, and how to build guardrails if you are wiring command generation into a tool or an agent.
Why generated shell commands go wrong
Most failures fall into a handful of predictable categories. Knowing them makes review much faster, because you know where to look.
- Platform differences. GNU and BSD tools disagree on flags.
sed -itakes an argument on macOS but not on most Linux systems.date -dexists in GNU coreutils but not in the BSDdatethat ships with macOS.xargs,stat, andfindall have similar splits. A model that does not know your platform will guess. - Quoting and word splitting. Filenames with spaces, quotes, or leading dashes break commands that look fine on simple test cases. Unquoted variables such as
rm -rf $DIR/*are the classic example: ifDIRis empty, the command becomesrm -rf /*. - Overly broad matches. A glob or a
findexpression that matches more than you meant, for example forgetting-type for using*.log*when you meant*.log. - Wrong shell. Syntax that works in bash or zsh fails or behaves differently in
sh, fish, or PowerShell. Arrays,[[ ]], and brace expansion are common culprits. - Hidden side effects. Commands that look read-only but are not, such as a
findwith-deleteplaced in the wrong position, agitcommand that rewrites history, or acurlthat uploads a file. - Invented flags. Models sometimes produce plausible-looking options that a tool does not support. Usually this just errors out, but occasionally a made-up flag collides with a real one that does something else.
Step 1: Give the model your environment up front
Most platform bugs disappear when the prompt says exactly where the command will run. A short preamble is enough:
Environment: Ubuntu 24.04, bash 5.2, GNU coreutils and findutils.
Working directory: /var/log/myapp (I do not have root).
Task: compress every *.log file larger than 100 MB that has not been
modified in 30 days. Keep the originals until I confirm.
Constraints: must handle filenames with spaces. No sudo.
Output: the command, then a line-by-line explanation of each part.
Three parts of this prompt matter most. The OS and shell remove guessing about flags. The working directory and privilege level set the blast radius. Asking for an explanation forces the model to state what each piece does, which gives you something concrete to check instead of a wall of punctuation.
If you use an AI assistant inside your terminal, check whether it already passes your OS and shell automatically. Many do, but it is worth confirming once rather than assuming.
Step 2: Read the explanation, then read the command
Do not run a command you could not explain to a coworker. The model's explanation is a starting point, not proof, so compare it against the command itself. A useful habit is to scan for these tokens before anything else:
rm,-delete,shred,truncate,dd,mkfs- Redirections that overwrite:
>rather than>>, andteewithout-a sudo,chmod -R,chown -Rgit push --force,git reset --hard,git clean -fd- Anything that talks to the network:
curl,wget,scp,nc, especially with upload flags - Pipes into an interpreter:
| sh,| bash,| python
If you see any of these and did not expect them, stop and ask the model why they are there. Often the answer reveals a misunderstanding of the task.
For flags you do not recognize, check the real documentation on your machine with man find or find --help. This takes seconds and catches invented options. The ShellCheck linter is also worth installing. It flags unquoted variables, unsafe globs, and many portability issues, and it works on one-liners pasted into a file as well as on full scripts.
Step 3: Turn destructive commands into dry runs
The single most useful technique is to split "select" from "act." Run the part that chooses targets first, look at the list, and only then add the action.
For the log compression example, a model might produce:
find . -type f -name '*.log' -size +100M -mtime +30 -print0 | xargs -0 gzip -k
Before running it, drop the action and just print the matches:
find . -type f -name '*.log' -size +100M -mtime +30 -print
If the list looks right, run the full command. If it is longer than expected, or includes files from a directory you forgot existed, you just avoided a mistake for free.
Many tools also have a built-in preview mode. It is worth asking the model to use them:
rsync --dry-run(or-n) shows what would be copied or deleted.git clean -nlists files thatgit clean -fwould remove.rm -iormv -iprompt before each change.sedwithout-iprints the result to the screen instead of editing in place.- For loops, replace the action with
echofirst:for f in *.png; do echo mv "$f" "${f%.png}.jpg"; done
You can make this the default by adding a line to your prompt: "If the command modifies or deletes anything, give me a dry-run version first."
Step 4: Test in a throwaway directory
For anything that renames, moves, or rewrites many files, build a tiny copy of the situation and run the command there first:
mkdir -p /tmp/cmdtest && cd /tmp/cmdtest
touch 'normal.log' 'with space.log' -- '-starts-with-dash.log'
# paste the generated command here and inspect the result
ls -la
Include the awkward cases on purpose: spaces, leading dashes, quotes, empty directories, and symlinks if they exist in your real data. These are exactly the inputs where generated commands tend to break. A test directory takes less than a minute to set up and is far cheaper than restoring from backup.
For bigger jobs, a container is even better. Running the command inside a disposable Docker container with a copy of the data mounted read-only, or a scratch copy mounted read-write, means a bad command cannot touch the original.
Step 5: Never pipe model output straight into a shell
It is tempting to build a helper like ai "do X" | bash. Do not. A pipe into a shell removes the review step entirely, and it means anything the model produces runs immediately, including commands influenced by text it read from a web page, a README, or a file you asked it to summarize. That last point matters: if the model's input includes untrusted content, an attacker can try to plant instructions in that content. This is the same prompt injection problem that affects AI agents, just in a smaller package.
A safer pattern for a terminal helper is to put the suggested command on your prompt line without executing it, so you press Enter yourself. In zsh you can do this with print -z; in bash, many tools write the command into the readline buffer or the clipboard instead. The extra keystroke is the whole point.
Building command generation into a tool or agent
If you are not just asking for one-liners but wiring an LLM into a product that runs shell commands, review by eye is not enough. You need guardrails in code.
Run with least privilege
Execute generated commands as a dedicated user with access only to the directories the task needs. Never run them as root. Where possible, run inside a sandbox such as a container with no network access by default, a read-only root filesystem, and resource limits on CPU, memory, and runtime.
Classify before you execute
Parse the command and sort it into tiers. A simple approach:
- Read-only (
ls,cat,grep,findwithout-deleteor-exec): allowed automatically. - Local writes inside the working directory: allowed with logging, or after a dry run.
- Destructive, privileged, or networked (
rm -r,sudo,curluploads,git push): require explicit human approval every time.
Use a real shell parser for this rather than regular expressions, because commands can hide behavior in subshells, $(...) substitutions, aliases, and -exec clauses. When the parser cannot confidently classify a command, treat it as the highest-risk tier. Failing closed is the correct default.
Prefer structured tools over free-form shell
If your agent only ever needs to list files, read files, and search text, give it those as separate tools with typed arguments instead of a general shell. A search_files(pattern, path) tool cannot accidentally run rm. Reserve free-form shell access for cases that genuinely need it.
Log everything
Record the user request, the generated command, who approved it, the exit code, and a truncated copy of the output. When something goes wrong, this is how you tell whether the model misunderstood the request, the classifier missed a risk, or the environment changed underneath you.
A quick checklist
- Tell the model your OS, shell, working directory, and privilege level.
- Ask for a line-by-line explanation and compare it with the command.
- Scan for deletion, overwrite, privilege, force, and network tokens.
- Check unfamiliar flags with
manor--help, and run ShellCheck on anything non-trivial. - Run the selection part on its own, or use the tool's dry-run mode, before acting.
- Test bulk changes in a scratch directory with awkward filenames.
- Never pipe model output directly into a shell.
- In products and agents, use least privilege, a sandbox, risk tiers, human approval for destructive actions, and full logs.
The bottom line
LLMs are genuinely good at remembering the exact find, awk, or ffmpeg incantation you need, and that saves real time. The trade-off is that a shell gives no second chances. Treat every generated command as a draft written by a fast but unfamiliar colleague: read it, preview it, test it on something you can throw away, and only then point it at the real thing. The routine adds a minute or two, and it is the difference between a useful assistant and an expensive cleanup.
Comments
Post a Comment