DEV Community

Taylor Wang
Taylor Wang

Posted on

Treat AI-Suggested Shell Commands Like a Merge Request, Not a Copy-Paste

The riskiest output from an AI coding session is not the function it wrote; it is the shell command you run in your terminal because it looks reasonable and it bypasses every review layer you already built for code. The moment you copy a generated one-liner into a shell, you are merging an unreviewed change directly into your machine, and often with more privilege than any branch of your repository ever gets. The conclusion you can act on is simple: treat every AI-suggested shell command as a merge request, run it against an isolated scratch environment first, and compare what actually changed against what you intended before letting it touch your real state.

The reason eyeballing fails is not that commands are hidden. A generated command is usually legible, often with helpful comments, but the danger is in adjacent effects that are not visible in the string. A pip install can upgrade a shared dependency in your current environment; a find . -name '*.tmp' -delete can remove a file outside the directory you intended if the working directory has shifted; a git clean -fdx can delete ignored files you needed when run one directory too high. Redirects and environment changes compound the problem. You might notice an obvious rm -rf and still miss a trailing --force on a package manager or a curl | bash that executes whatever the remote server decides to send at run time. That is the same class of risk people now worry about when AI agents hold filesystem or browser tools, except the agent here is you pasting the command.

The preflight workflow is small enough to become a habit. When you get a shell command from a model, ask for it in two parts: the command itself and a one-sentence intent note that says what should change, what should not change, and which directories or services the command is allowed to touch. If you have free model access, you can make that intent note part of the same prompt and let the model revise the command until the note is precise. Then run the command through a tiny preflight script that does not execute it on your real HOME, but instead creates a temporary directory, repoints HOME, TMPDIR, and the working directory into that scratch space, executes the command with a short timeout, and hashes the files before and after so you can see exactly what changed. You are not looking for a proof that the command is safe; you are looking for a diff that you can compare against the intent note.

Here is a compact Python preflight you can keep in a bin directory and run on any Unix machine. It is intentionally simple: it blocks a few obvious dangerous tokens, runs the command in a temporary directory, and reports the exit code along with created and changed files. It is a difference collector, not a sandbox, and the surrounding text matters more than the code.

#!/usr/bin/env python3
import os, sys, subprocess, tempfile

def walk(root):
    for dirpath, _, filenames in os.walk(root):
        for name in filenames:
            yield os.path.join(dirpath, name)

def hash_file(path):
    try:
        with open(path, "rb") as f:
            data = f.read()
        import hashlib
        return hashlib.sha256(data).hexdigest()
    except OSError:
        return "unreadable"

blocked = ("rm -rf /", ":(){", "sudo ", "mkfs", "dd if=")
command = sys.argv[1]
if any(token in command for token in blocked):
    print("blocked by preflight token check")
    sys.exit(2)

with tempfile.TemporaryDirectory(prefix="ai-preflight-") as temp:
    env = {**os.environ, "HOME": temp, "TMPDIR": temp, "PWD": temp}
    before = {p: hash_file(p) for p in walk(temp)}
    try:
        proc = subprocess.run(
            ["bash", "-lc", command],
            cwd=temp,
            env=env,
            capture_output=True,
            text=True,
            timeout=20,
        )
    except subprocess.TimeoutExpired:
        print("timed out; do not run this command on real state")
        sys.exit(3)
    after = {p: hash_file(p) for p in walk(temp)}
    print(f"exit={proc.returncode}")
    print("created:", sorted(after.keys() - before.keys()))
    print("changed:", sorted(p for p in before.keys() & after.keys() if before[p] != after[p]))
    if proc.stdout:
        print("stdout:", proc.stdout[-400:])
    if proc.stderr:
        print("stderr:", proc.stderr[-400:])
Enter fullscreen mode Exit fullscreen mode

Run it with the generated command as a single quoted argument, and then read the output the way you would read a code review comment: does the list of created files match the intent note, and does the changed-file list contain anything that is not explained. A package manager command that creates a lock file in the temporary directory is probably fine; a command that somehow reaches into your real home or project directory is not, which is why this script should be the first signal rather than the final permission. For anything that performs network mutations, deletes data, or uses credentials, you need a stronger boundary such as a container or a dedicated VM with network egress restrictions, and you still need human review.

This is where a free model and a free server option become practical rather than promotional. If you are already generating the command through a model, you can use free model access to generate the paired intent note without adding another paid call, and then you can use the free server option to keep the preflight runner available for a debugging session without tying the check to your laptop. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow does not depend on MonkeyCode; the same script works with any model and any machine. But the free access removes two ordinary excuses for skipping the check: the cost of an extra model round trip and the cost of a place to run it.

There are clear limits you should accept before using this approach. The temporary directory is not a sandbox. Commands can still reach absolute paths, read environment variables, make outbound requests, or modify global package caches if they are invoked in a way that escapes the small redirection above. The static token check is easy to bypass with indirection, encoding, or multi-stage downloaders. A command that works in an empty scratch directory may fail in your real project because the preflight did not reproduce the exact state, installed tools, or network conditions. Therefore this script is not the right tool for commands that touch production systems, move real user data, or require secrets; those should go through the same change-management process as a code deployment, including a peer review and a proper staging environment. The person who should not use this approach is the one who wants a single command to declare an AI suggestion safe, because no such command exists.

The workflow is valuable precisely because it changes the default from trust to evidence. When an AI returns a plausible shell command, you stop asking whether it looks safe and start asking whether the observed diff matches the intended diff. That small shift is enough to catch the adjacent effects that usually cause damage, and it costs less than a minute once the preflight script is in your path. Try it on the next AI-generated one-liner you were about to paste, and if the reported diff does not match your intent, delete the command rather than debugging it on your real machine.

Top comments (0)