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 fzf
    
  • Fedora/RHEL/CentOS Stream (dnf):

    sudo dnf install -y curl jq fzf
    
  • openSUSE/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 podman
    
  • Fedora/RHEL/CentOS Stream:

    sudo dnf install -y podman
    
  • openSUSE/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 | sh
    
  • Enable and start the service (systemd):

    sudo systemctl enable --now ollama
    
  • Pull a model (balanced option):

    ollama pull llama3.1
    

    You can also use smaller models like llama3.2:3b on 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 -r on by default on production servers; require -y to override.
    • For writes, ask it to propose a backup step and a rollback hint.
    • Pipe package installs through --downloadonly/--assumeno equivalents when available.
  • 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.
  • 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 ollama
    
  • Model not downloaded:

    ollama pull llama3.1
    
  • JSON 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 fzf for 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.