- Posted on
- • Artificial Intelligence
Building an Artificial Intelligence Copilot for Linux Servers
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Building an Artificial Intelligence Copilot for Linux Servers
Ever been on-call at 2 a.m., staring at a blinking cursor, knowing what you want but not the exact incantation? “Rotate nginx logs, compress yesterday’s, and reload gracefully. Also, don’t break anything.” An AI copilot for your Linux shell can turn those plain-English intents into safe, auditable commands—complete with explanations and guardrails.
This article shows you how to build a local-first Bash copilot that:
Suggests commands from natural language
Explains what it’s doing and why
Asks for confirmation before running anything risky
Logs everything for audit and repeatability
You’ll get a working script, installation instructions for apt, dnf, and zypper, and examples to make it useful on day one.
Why a shell copilot is worth it
Reduced cognitive load: You describe goals; it proposes commands and flags.
Fewer footguns: Prompts to confirm before destructive actions; optional “read-only” mode.
Faster onboarding: Junior admins can learn by seeing AI-proposed commands and explanations.
Local-first option: Use open models via Ollama for speed and privacy, or switch to a cloud API when needed.
Core idea and architecture (in 60 seconds)
You type: a natural-language task.
The copilot gathers minimal context (user, cwd), asks an LLM to propose a command and explanation, and returns structured JSON.
You confirm; the script executes and logs stdout/stderr and exit status.
Optionally, it summarizes the result.
We’ll support two backends:
Local: Ollama (runs models like Llama 3 on your machine)
Cloud: OpenAI API (if you have a key and prefer offloading to the cloud)
1) Install prerequisites
We’ll need curl, jq, and optionally fzf (for history search later). Pick your distro’s package manager:
Debian/Ubuntu (apt):
sudo apt update sudo apt install -y curl jq fzfFedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y curl jq fzfopenSUSE/SLE (zypper):
sudo zypper refresh sudo zypper install -y curl jq fzf
Optional for sandboxing tasks in containers (you can skip this for now):
Debian/Ubuntu:
sudo apt install -y podmanFedora/RHEL/CentOS Stream:
sudo dnf install -y podmanopenSUSE/SLE:
sudo zypper install -y podman
2) Choose and set up your LLM backend
Option A: Local-first with Ollama (recommended to start)
Install Ollama:
curl -fsSL https://ollama.com/install.sh | shEnable and start the service (systemd):
sudo systemctl enable --now ollamaPull a model (balanced option):
ollama pull llama3.1You can also use smaller models like
llama3.2:3bon modest machines.
Option B: Cloud API (OpenAI)
- Export your API key and choose a model:
export OPENAI_API_KEY="sk-...yourkey..." export OPENAI_MODEL="gpt-4o-mini"You can set these in your shell profile for persistence (e.g., ~/.bashrc).
3) Create the Bash copilot script
Save this as ~/bin/copilot and make it executable:
mkdir -p ~/bin
cat > ~/bin/copilot <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
# Minimal, local-first AI copilot for Bash.
# Backends: Ollama (default), or OpenAI if AI_BACKEND=openai.
AI_BACKEND="${AI_BACKEND:-ollama}"
OLLAMA_MODEL="${OLLAMA_MODEL:-llama3.1}"
OPENAI_MODEL="${OPENAI_MODEL:-gpt-4o-mini}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/bash-copilot"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bash-copilot"
LOG_DIR="$DATA_DIR/logs"
HIST_FILE="$DATA_DIR/history.jsonl"
mkdir -p "$CONFIG_DIR" "$LOG_DIR"
usage() {
cat <<USAGE
Usage: copilot [-y] [-r] [--] <task description>
-y Auto-confirm execution (non-interactive)
-r Read-only mode: refuse likely destructive commands
(e.g., rm, mkfs, dd, :(){}, chmod -R, chown -R, package mgr writes, systemctl stop/disable, etc.)
-- End of options
Examples:
copilot "check free space under /var and show largest dirs"
copilot -r "preview changes to rotate nginx logs, no writes"
USAGE
}
AUTO_Y=0
READ_ONLY=0
while [[ "${1:-}" == -* ]]; do
case "$1" in
-y) AUTO_Y=1; shift;;
-r) READ_ONLY=1; shift;;
--) shift; break;;
-h|--help) usage; exit 0;;
*) echo "Unknown option: $1" >&2; usage; exit 2;;
esac
done
if [[ $# -lt 1 ]]; then
usage; exit 2
fi
TASK="$*"
USER_INFO="$(whoami)@$(hostname)"
CWD="$(pwd)"
SHELL_NAME="${SHELL:-/bin/bash}"
DATE_ISO="$(date -Is)"
SYSTEM_PROMPT=$'You are a Linux shell copilot. Output ONLY a single JSON object with keys:\n\
- cmd: the safest, least-destructive Bash command(s) to achieve the user\'s task, optimized for portability.\n\
- explain: short explanation of why this command was chosen.\n\
- danger: array of risks/caveats.\n\
Constraints:\n\
- Prefer read-only or preview flags (e.g., grep, du, find, --dry-run) unless user explicitly permits writes.\n\
- Never include placeholders like <password>. Do not invent paths; verify with safe listing if unsure.\n\
- If action likely requires sudo, include it but keep it minimal and explain why.\n\
- Combine commands with && and set -euo pipefail where appropriate.\n\
- Use sh-compatible syntax when possible.\n\
- No markdown, no code fences, JSON only.'
USER_PROMPT=$(cat <<PROMPT
User: $USER_INFO
Shell: $SHELL_NAME
CWD: $CWD
Datetime: $DATE_ISO
Task: $TASK
PROMPT
)
json_query_ollama() {
local sys="$1" usr="$2"
curl -s http://localhost:11434/api/generate \
-d "$(jq -n --arg model "$OLLAMA_MODEL" --arg prompt "$usr" --arg system "$sys" \
'{model:$model, prompt:$prompt, system:$system, stream:false}')" \
| jq -r '.response'
}
json_query_openai() {
local sys="$1" usr="$2"
curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer ${OPENAI_API_KEY:?Set OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg model "$OPENAI_MODEL" \
--arg sys "$sys" --arg usr "$usr" \
'{model:$model, temperature:0.2, messages:[{"role":"system","content":$sys},{"role":"user","content":$usr}]}')" \
| jq -r '.choices[0].message.content'
}
RESP_RAW=""
case "$AI_BACKEND" in
ollama) RESP_RAW="$(json_query_ollama "$SYSTEM_PROMPT" "$USER_PROMPT")";;
openai) RESP_RAW="$(json_query_openai "$SYSTEM_PROMPT" "$USER_PROMPT")";;
*) echo "Unsupported AI_BACKEND: $AI_BACKEND" >&2; exit 3;;
esac
# Ensure response is JSON
if ! echo "$RESP_RAW" | jq -e . >/dev/null 2>&1; then
echo "Model did not return valid JSON. Raw response:" >&2
echo "$RESP_RAW" >&2
exit 4
fi
CMD="$(echo "$RESP_RAW" | jq -r '.cmd')"
EXPLAIN="$(echo "$RESP_RAW" | jq -r '.explain')"
DANGER="$(echo "$RESP_RAW" | jq -r '.danger | join("; ")' 2>/dev/null || true)"
if [[ -z "$CMD" || "$CMD" == "null" ]]; then
echo "No command proposed by the model." >&2
echo "$RESP_RAW" >&2
exit 5
fi
# Simple read-only guardrail
is_destructive() {
local c="$1"
grep -Eq '\b(rm|mkfs|dd\s|:\(\)\s*{\s*:\s*\|:\s*&\s*};:|chmod\s+-R|chown\s+-R|chgrp\s+-R|wipefs|parted|pvcreate|vgcreate|lvcreate|lvremove|zpool|zfs\s+destroy|truncate\s+-s|sed\s+-i|perl\s+-pi|tee\s+[^>]*>|>|>>|curl\s+.*\|\s*sh|bash\s+<(.*)|userdel|groupdel|reboot|poweroff|shutdown|systemctl\s+(stop|disable)|apt(-get)?\s+(install|remove|purge)|dnf\s+(install|remove)|zypper\s+(install|remove))\b' <<<"$c"
}
if (( READ_ONLY )) && is_destructive "$CMD"; then
echo "Refusing to run potentially destructive command in read-only mode:"
echo " $CMD"
exit 6
fi
echo
echo "Proposed command:"
echo " $CMD"
echo
echo "Why:"
echo " $EXPLAIN"
if [[ -n "$DANGER" && "$DANGER" != "null" ]]; then
echo "Risks/Caveats:"
echo " $DANGER"
fi
echo
ANS="n"
if (( AUTO_Y )); then
ANS="y"
else
read -r -p "Run this? [y/N] " ANS
fi
LOG_TS="$(date +%Y%m%d-%H%M%S)"
RUN_LOG="$LOG_DIR/run-$LOG_TS.log"
if [[ "$ANS" =~ ^[Yy]$ ]]; then
echo
echo "Executing..."
set +e
bash -lc "$CMD" 2>&1 | tee "$RUN_LOG"
STATUS=${PIPESTATUS[0]}
set -e
echo
echo "Exit status: $STATUS (log: $RUN_LOG)"
else
STATUS=125
echo "Skipped execution."
fi
# Append structured history
jq -n --arg ts "$DATE_ISO" --arg task "$TASK" --arg cmd "$CMD" \
--arg explain "$EXPLAIN" --arg danger "$DANGER" \
--arg log "$RUN_LOG" --argjson status "$STATUS" \
'{time:$ts, task:$task, cmd:$cmd, explain:$explain, danger:$danger, log:$log, status:$status}' \
>> "$HIST_FILE"
exit "$STATUS"
EOF
chmod +x ~/bin/copilot
Add a convenience function and PATH update to your ~/.bashrc:
# Ensure ~/bin is on PATH
[[ ":$PATH:" != *":$HOME/bin:"* ]] && export PATH="$HOME/bin:$PATH"
# Handy alias
alias ai='copilot'
Restart your shell or source ~/.bashrc.
4) Try it: real-world examples
These prompts illustrate the value on day one. The copilot will propose a command, explain it, and ask before running.
Find where disk space is going under /var
ai "Show top 10 largest directories under /var, human-readable"Nginx log rotation preview (read-only)
ai -r "Rotate nginx logs safely and compress yesterday's, but only show what would happen"Investigate high load without changing anything
ai -r "Show top CPU-hogging processes and summarize by user"Create a systemd service (it should propose creating a file and reloading daemon; review carefully)
ai "Create a simple systemd service to run /opt/app/start.sh as appuser and enable it at boot"Open firewall for Postgres port 5432 (copilot should check your firewall toolset; confirm carefully)
ai "Allow inbound TCP 5432 persistently using firewall-cmd on RHEL/Fedora"
Tip: If the command looks risky, re-run with -r, or modify the prompt: “show dry run,” “preview only,” “don’t write.”
5) Upgrade ideas and safety tips
Context boosts accuracy:
- Prepend “We’re on Ubuntu 22.04” or “This is Fedora 40” to the prompt.
- Add “Use POSIX sh-compatible syntax.”
Stronger guardrails:
- Keep
-ron by default on production servers; require-yto override. - For writes, ask it to propose a backup step and a rollback hint.
- Pipe package installs through
--downloadonly/--assumenoequivalents when available.
- Keep
Audit and collaboration:
- The script writes JSONL history to
~/.local/share/bash-copilot/history.jsonl. - Pipe that file into grep/jq for reporting or share curated snippets with your team.
- The script writes JSONL history to
Sandbox execution (optional):
- Preface commands with Podman to test filesystem effects:
podman run --rm -it --net=host -v "$PWD":"$PWD" -w "$PWD" registry.fedoraproject.org/fedora:40 bash -lc '<cmd>'- Ask the copilot: “Propose the same, but run inside a rootless podman container.”
Swap models on the fly:
OLLAMA_MODEL="llama3.2:3b" ai "summarize dmesg errors" AI_BACKEND=openai OPENAI_MODEL="gpt-4o-mini" ai "generate a safe rsync command with progress and retries"
Troubleshooting
Ollama connection refused:
sudo systemctl status ollama sudo systemctl enable --now ollamaModel not downloaded:
ollama pull llama3.1JSON parsing error: The model may have emitted extra text. Re-run the command, or add “Respond with JSON only” to your prompt.
Conclusion and next steps
With a few packages and a ~200-line script, you now have a practical AI copilot for Linux that helps you move faster and safer. Start using it for read-only diagnostics, then graduate to controlled changes with backups and rollbacks.
Where to go from here:
Turn on history search with
fzffor past tasks.Add SSH fleet support to run the confirmed command across hosts with
pssh/parallel-ssh.Extend the JSON schema to include “pre-checks,” “post-checks,” and “rollback.”
Your next move: install the prerequisites, pick a backend (Ollama or OpenAI), drop in the script, and try your first prompt:
ai -r "List largest files modified in the last 24 hours under /var/log without changing anything"
Build confidence in read-only mode—then let your copilot take on bigger jobs, one confirmed step at a time.