- Posted on
- • Artificial Intelligence
Prompt Engineering for Sysadmins
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Prompt Engineering for Sysadmins: Turning LLMs into Reliable Bash Assistants
Ever stared at a blank terminal at 3 a.m., knowing there’s a one-liner somewhere that could save your night—but not remembering it? Large Language Models (LLMs) can help, but only if you ask the right way. This post shows how to use prompt engineering, structure, and guardrails to safely integrate LLMs into a Linux sysadmin workflow—so you get accurate, auditable, and reproducible outcomes, not mysterious copy-pasta.
What you’ll get:
Why prompt engineering matters for ops
A minimal, practical toolchain
3–5 concrete patterns you can put into production today
Real code you can run, test in containers, and version with your team
Prerequisites (CLI-first, distro-friendly)
We’ll use only ubiquitous tools: curl, jq, shellcheck, and podman. Install with your package manager:
curl
- apt:
sudo apt update && sudo apt install -y curl - dnf:
sudo dnf install -y curl - zypper:
sudo zypper refresh && sudo zypper install -y curl
- apt:
jq
- apt:
sudo apt install -y jq - dnf:
sudo dnf install -y jq - zypper:
sudo zypper install -y jq
- apt:
ShellCheck (Bash linter)
- apt:
sudo apt install -y shellcheck - dnf:
sudo dnf install -y ShellCheck - zypper:
sudo zypper install -y ShellCheck
- apt:
Podman (for safe sandboxes)
- apt:
sudo apt install -y podman - dnf:
sudo dnf install -y podman - zypper:
sudo zypper install -y podman
- apt:
If you plan to call a hosted LLM (e.g., OpenAI), export your API key:
export OPENAI_API_KEY="sk-...your-key..."
You can also point at any OpenAI-compatible endpoint with:
export API_URL="https://api.openai.com/v1/chat/completions"
export MODEL="gpt-4o-mini"
Why prompt engineering is worth your time
Accuracy beats guesswork: Clear, constrained prompts dramatically reduce hallucinations and irrelevant output.
Speed with safety: Force machine-readable responses and automate validation before execution.
Auditability and reproducibility: Keep prompts, model outputs, and changes in Git; test in containers before touching prod.
Team scalability: A shared prompt library makes senior expertise reusable (and safer) for everyone.
Pattern 1: The “Ops Prompt” Template (constraints > vibes)
Be explicit. Tell the model your environment, the task, the allowed operations, and the expected output format.
Copy this into a file like prompts/rsync-backup.txt and customize:
You are a senior Linux sysadmin assistant. Produce a safe, minimal plan to create an rsync-based backup from /var/www to /backups/www with:
- incremental backups
- file permissions preserved
- excludes: node_modules, .git
- dry-run first
- no destructive actions
- Debian/Ubuntu family paths/tools
Output strictly as JSON (no markdown, no explanations), with this structure:
{
"actions": [
{"type":"command","cmd":"..."},
{"type":"script","filename":"...","language":"bash","body":"..."}
],
"notes":"short plain-text reasoning about assumptions and risks"
}
Rules:
- Only include commands that are idempotent or explicitly use --dry-run first.
- Prefer long-form flags for clarity.
- Never include 'rm -rf' or anything that modifies / outside target paths.
This kind of prompt:
States OS assumptions (Debian/Ubuntu)
Limits scope (what not to do)
Requires a structured, machine-parseable plan
Pattern 2: Force structured output and parse it with jq
Use a thin CLI wrapper to call your LLM and demand JSON-only output. Here’s a minimal script that works with OpenAI-compatible APIs.
Save as scripts/llm.sh:
#!/usr/bin/env bash
set -euo pipefail
API_URL="${API_URL:-https://api.openai.com/v1/chat/completions}"
MODEL="${MODEL:-gpt-4o-mini}"
prompt_file="${1:-}"
if [[ -z "${prompt_file}" ]]; then
echo "Usage: $0 PROMPT_FILE" >&2
exit 1
fi
prompt="$(cat "$prompt_file")"
curl -sS "$API_URL" \
-H "Authorization: Bearer ${OPENAI_API_KEY:?Set OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg m "$MODEL" --arg p "$prompt" '{
model: $m,
messages: [{role:"user", content:$p}],
temperature: 0,
response_format: {type:"json_object"}
}')" \
| jq -r '.choices[0].message.content'
Usage:
bash scripts/llm.sh prompts/rsync-backup.txt > plan.json
jq . plan.json
Now you can safely pipe the plan into tooling without scraping prose.
Pattern 3: Review, lint, confirm, and only then run
A “review and run” gate keeps you honest. It shows what the model wants to do, lints scripts with ShellCheck, and asks for confirmation. It can also run in an isolated container.
Save as scripts/review-run.sh:
#!/usr/bin/env bash
set -euo pipefail
PLAN="${1:-plan.json}"
DRY_RUN="${DRY_RUN:-0}"
SANDBOX_IMAGE="${SANDBOX_IMAGE:-}"
if [[ ! -f "$PLAN" ]]; then
echo "Plan file not found: $PLAN" >&2
exit 1
fi
echo "== Plan summary =="
jq -r '
.actions[] |
if .type=="command" then
"COMMAND: " + .cmd
elif .type=="script" then
"SCRIPT: " + .filename
else
"UNKNOWN ACTION"
end
' "$PLAN" || true
echo
echo "== Notes =="
jq -r '.notes // "(no notes)"' "$PLAN"
echo
# Materialize scripts and lint them
mapfile -t scripts < <(jq -r '.actions[] | select(.type=="script") | @base64' "$PLAN")
for s in "${scripts[@]}"; do
_jq() { echo "$s" | base64 --decode | jq -r "$1"; }
fname=$(_jq '.filename')
body=$(_jq '.body')
lang=$(_jq '.language')
echo "Writing script: $fname"
printf "%s\n" "$body" > "$fname"
chmod +x "$fname"
if command -v shellcheck >/dev/null 2>&1 && [[ "$lang" == "bash" ]]; then
echo "Linting $fname with ShellCheck..."
if ! shellcheck "$fname"; then
echo "ShellCheck reported issues. Review before continuing." >&2
fi
else
echo "ShellCheck not available or non-bash script. Skipping lint."
fi
done
echo
read -rp "Proceed to execute commands? [y/N] " ans
if [[ ! "$ans" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
run_cmd() {
local cmd="$1"
if [[ -n "$SANDBOX_IMAGE" ]]; then
# Run in a disposable container, mount PWD for context
podman run --rm -v "$PWD":/work -w /work "$SANDBOX_IMAGE" bash -lc "$cmd"
else
bash -lc "$cmd"
fi
}
# Execute commands in order
mapfile -t actions < <(jq -r '.actions[] | @base64' "$PLAN")
for a in "${actions[@]}"; do
_jq() { echo "$a" | base64 --decode | jq -r "$1"; }
type=$(_jq '.type')
case "$type" in
command)
cmd=$(_jq '.cmd')
echo "+ $cmd"
if [[ "$DRY_RUN" == "1" ]]; then
echo "(dry run) not executing"
else
run_cmd "$cmd"
fi
;;
script)
fname=$(_jq '.filename')
echo "+ running script: $fname"
if [[ "$DRY_RUN" == "1" ]]; then
echo "(dry run) not executing $fname"
else
if [[ -n "$SANDBOX_IMAGE" ]]; then
podman run --rm -v "$PWD":/work -w /work "$SANDBOX_IMAGE" bash -lc "./$fname"
else
"./$fname"
fi
fi
;;
*)
echo "Skipping unknown action type: $type"
;;
esac
done
echo "Done."
Usage examples:
Normal mode on host:
bash scripts/review-run.sh plan.jsonDry run:
DRY_RUN=1 bash scripts/review-run.sh plan.jsonIn a sandbox (recommended):
SANDBOX_IMAGE=ubuntu:24.04 bash scripts/review-run.sh plan.json
Pattern 4: Test changes in a disposable container
Before touching a real server, validate in a clean OS image.
- One-off interactive shell:
podman run --rm -it ubuntu:24.04 bash
- Run a generated command in a container:
podman run --rm ubuntu:24.04 bash -lc 'apt-get update && apt-get install -y rsync && rsync --version'
- Full plan execution in sandbox:
SANDBOX_IMAGE=ubuntu:24.04 bash scripts/review-run.sh plan.json
This prevents “works on my machine” surprises and catches missing packages or distro differences early.
Pattern 5: Reusable prompts in Git (with ops facts)
Keep prompts, runs, and notes under version control:
Store prompts in prompts/*.txt with placeholders for environment, paths, SLAs.
Keep generated plans (plan-YYYYmmdd.json) as part of audit logs.
Check scripts into a “scratch” or “ops-lab” repo, never directly into prod repos without review.
Use PRs for changes derived from LLM output, and staple the prompt + plan into the PR description.
Example “facts” snippet you can prepend to any prompt:
Environment facts:
- Distro: Ubuntu 22.04 LTS
- Shell: /bin/bash
- Package manager: apt
- Sensitive directories: /etc, /var/lib, /var/backups
- Network egress: allowed to internal mirrors only
- Expect non-interactive commands (assume CI)
Real-world example: Safe rsync backup plan
1) Write the prompt (prompts/rsync-backup.txt) using Pattern 1.
2) Generate the plan:
bash scripts/llm.sh prompts/rsync-backup.txt > plan.json
jq . plan.json
3) Review and test in a sandbox first:
SANDBOX_IMAGE=ubuntu:24.04 bash scripts/review-run.sh plan.json
4) If the plan includes rsync and your sandbox image doesn’t have it, the run will fail fast—good! Fix the plan or adjust the sandbox:
podman run --rm ubuntu:24.04 bash -lc 'apt-get update && apt-get install -y rsync'
Then rerun the review-run script.
5) Once validated, run on the target host with DRY_RUN=1 first, then without.
Troubleshooting and tips
If you see markdown in the model output, your prompt didn’t insist on JSON-only or your API doesn’t support response_format. Add “Output strictly as JSON (no markdown or code fences).”
Add package installs to actions when needed. Example:
{"type":"command","cmd":"apt-get update && apt-get install -y rsync"}
Make destructive operations opt-in. Require the model to propose a dry-run first, then a confirmed action.
Lint everything. ShellCheck catches redirections, quoting issues, and unbound variables before they bite you.
Conclusion and Call to Action
LLMs can be excellent junior operators—if you treat them like one: give tight instructions, demand structured output, lint and sandbox their work, and keep an audit trail. Start small:
Install curl, jq, ShellCheck, and Podman.
Create your prompts/ and scripts/ folders from this post.
Pick one recurring task (backups, log rotation, user provisioning) and run it end-to-end with plan.json + review-run.sh.
Once it sticks, share your prompt library with your team and standardize the review-run flow. Your future 3 a.m. self will thank you.