Posted on
Artificial Intelligence

AI Agents with Bash

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

AI Agents with Bash: Turn Your Shell Into an Automation Copilot

If you’ve been watching the rise of “AI agents,” you might think you need a complex framework, a web UI, or a Kubernetes cluster to get value. Spoiler: you don’t. With Bash, curl, and jq, you can build a practical, auditable AI agent that plans and executes shell commands to help with everyday ops and data-wrangling tasks—right on your workstation.

This post shows you how to wire an LLM into your terminal safely, why it’s worth doing, and gives you a minimal, extensible agent you can run today.

Why build AI agents in Bash?

  • Ubiquity and composability: Bash is everywhere. Your tools—grep, jq, find, systemctl—already solve 80% of problems. An LLM can plan how to chain them.

  • Auditability and control: Everything is plain text. You can log prompts, outputs, and commands. Nothing runs unless you say so.

  • Privacy and cost: Use a local model via Ollama to keep data on-device. Or swap in a cloud endpoint if you need stronger reasoning.

  • Low friction: No new stack. A few functions and you’ve got a working agent loop you can customize.

What we’ll build

A tiny Bash agent that:

  • Takes a natural-language goal from you.

  • Asks an LLM to propose a shell command as JSON.

  • Shows you the plan, asks for confirmation, and executes in a guarded way.

  • Logs what happened for reproducibility and learning.

Prerequisites and installation

We’ll use curl and j q for HTTP and JSON parsing. Optional: ripgrep for fast text search.

Install these with your package manager:

  • Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y curl jq ripgrep
  • Fedora (dnf):
sudo dnf install -y curl jq ripgrep
  • openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y curl jq ripgrep

You have two choices for the LLM:

1) Local (recommended): Install Ollama and pull a model (e.g., mistral or llama3)

curl -fsSL https://ollama.com/install.sh | sh
# In one terminal, start the server:
ollama serve
# In another terminal, pull a model:
ollama pull mistral

2) Cloud (OpenAI-compatible): Set your environment

export OPENAI_API_KEY="sk-..."
# Optional, for other providers that expose an OpenAI-compatible API:
# export OPENAI_BASE_URL="https://api.openai.com/v1"
# export OPENAI_MODEL="gpt-4o-mini"   # or the model your provider supports

Note: Ollama isn’t generally available via apt/dnf/zypper. Use the vendor install script above.

Step 1 — A minimal LLM client in Bash

Create a new file called agent.sh:

#!/usr/bin/env bash
set -euo pipefail

require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing $1. Install it first."; exit 1; }; }
require_cmd curl
require_cmd jq

# Provider auto-detection
: "${OLLAMA_HOST:=http://127.0.0.1:11434}"
: "${OLLAMA_MODEL:=mistral}"
: "${OPENAI_BASE_URL:=https://api.openai.com/v1}"
: "${OPENAI_MODEL:=gpt-4o-mini}"

llm_chat() {
  # $1 = system prompt, $2 = user prompt
  if nc -z 127.0.0.1 11434 >/dev/null 2>&1; then
    curl -fsS "$OLLAMA_HOST/api/chat" \
      -H 'Content-Type: application/json' \
      -d "$(jq -nc --arg sys "$1" --arg usr "$2" --arg model "$OLLAMA_MODEL" \
           '{model:$model, stream:false, messages:[{role:"system",content:$sys},{role:"user",content:$usr}], options:{temperature:0.2}}')" \
      | jq -r '.message.content'
  elif [[ -n "${OPENAI_API_KEY:-}" ]]; then
    curl -fsS "$OPENAI_BASE_URL/chat/completions" \
      -H "Authorization: Bearer $OPENAI_API_KEY" \
      -H 'Content-Type: application/json' \
      -d "$(jq -nc --arg sys "$1" --arg usr "$2" --arg model "$OPENAI_MODEL" \
          '{model:$model, temperature:0.2, messages:[{role:"system",content:$sys},{role:"user",content:$usr}]}')" \
      | jq -r '.choices[0].message.content'
  else
    echo "No LLM provider available. Start Ollama or set OPENAI_API_KEY." >&2
    exit 1
  fi
}

This function speaks either to a local Ollama server or a cloud endpoint that implements the OpenAI API.

Step 2 — Add a safe “tool use” loop

We’ll instruct the model to respond with strict JSON that includes a single safe command proposal. We then validate against an allowlist and ask you to confirm.

Append to agent.sh:

ALLOWLIST_REGEX='^(ls|cat|grep|rg|sed|awk|jq|curl|find|head|tail|sort|uniq|wc|du|df|systemctl|journalctl|tar|gzip|zcat|zgrep)\b'

SYSTEM_PROMPT='You are a Bash assistant. 
Return ONLY a single-line JSON object with keys: thought, command, explain. 

- command MUST be a safe, non-destructive CLI pipeline that runs in a typical Linux shell.

- Prefer read-only commands (grep, jq, find, systemctl status, journalctl, sort, uniq, wc, head, tail).

- Never write or remove files, never "sudo", never background jobs, never edit configs.

- Use POSIX/Bash-friendly one-liners. 

- Do not include backticks or code fences.

- Keep command under 180 characters.'

propose_command() {
  local goal="$1"
  local resp
  resp="$(llm_chat "$SYSTEM_PROMPT" "Goal: $goal")"
  # Try to extract JSON even if the model adds text accidentally
  local json
  json="$(echo "$resp" | sed -n 's/.*{\(.*\)}/{\1}/p')"
  if [[ -z "$json" ]]; then
    echo "Model did not return JSON. Full response:" >&2
    echo "$resp" >&2
    exit 1
  fi
  echo "$json"
}

validate_and_confirm() {
  local cmd="$1"
  if ! [[ "$cmd" =~ $ALLOWLIST_REGEX ]]; then
    echo "Blocked: command not in allowlist. Proposed: $cmd" >&2
    exit 2
  fi
  echo "Plan: $cmd"
  read -rp "Run this command? [y/N] " ans
  [[ "${ans,,}" == "y" ]]
}

run_with_logging() {
  local goal="$1" cmd="$2"
  mkdir -p .agent_logs
  local ts
  ts="$(date +%Y%m%d-%H%M%S)"
  echo "# $ts goal: $goal" | tee ".agent_logs/$ts.txt" >/dev/null
  set +e
  bash -o pipefail -c "$cmd" 2>&1 | tee -a ".agent_logs/$ts.txt"
  local rc=$?
  set -e
  echo -e "$ts\t$rc\t$goal\t$cmd" >> ".agent_logs/history.tsv"
  return $rc
}

main() {
  if [[ $# -lt 1 ]]; then
    echo "Usage: $0 \"your goal in natural language\"" >&2
    exit 1
  fi
  local goal="$*"
  local json thought cmd explain
  json="$(propose_command "$goal")"
  thought="$(jq -r '.thought // ""' <<< "$json")"
  cmd="$(jq -r '.command // empty' <<< "$json")"
  explain="$(jq -r '.explain // ""' <<< "$json")"

  echo "Thought: $thought"
  echo "Explain: $explain"
  if [[ -z "$cmd" ]]; then
    echo "No command proposed." >&2
    exit 1
  fi

  if validate_and_confirm "$cmd"; then
    run_with_logging "$goal" "$cmd"
  else
    echo "Aborted."
  fi
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi

Make it executable:

chmod +x agent.sh

Try it:

./agent.sh "Summarize error rates from the last 100 systemd journal lines"

Example outcome:

  • The model might propose: journalctl -n 100 | grep -i "error" | sort | uniq -c | sort -nr | head

  • You confirm, it runs, and the output plus metadata are logged to .agent_logs/.

Step 3 — Real-world example: Log triage agent

Use the same script for guided diagnostics. Some prompts to try:

  • “Find the top 10 services with the most errors in the last 500 logs.”

  • “Show failing systemd units and the last error line for each.”

Examples:

./agent.sh "List failing systemd units and their last log line"
./agent.sh "From journalctl last 500 lines, show top 5 most frequent error messages"

These typically generate pipelines using systemctl --failed, journalctl, grep/rg, and uniq -c.

Tip: If your distribution uses systemd journals with compression, the allowlist includes zcat and zgrep.

Step 4 — Real-world example: Data wrangling assistant

Point the agent at CSVs or logs and let it suggest safe one-liners to inspect, count, and filter.

Prompts:

  • “Given access.log, show the top 10 IPs by request count.”

  • “Convert a small CSV to JSON lines (no file writes).”

  • “Find rows where column 3 > 100 and show the first 5.”

Runs like:

./agent.sh "From access.log, list top 10 IP addresses by frequency"
./agent.sh "Print the first 5 rows of data.csv as JSON objects using awk or jq (no writes)"

You’ll see pipelines with awk, cut, sort, uniq, jq -R -s, etc. Because we forbid writes, the agent will show previews without altering files.

Step 5 — Lightweight “memory” for traceability

The script appends a TSV line per run to .agent_logs/history.tsv:

  • Timestamp

  • Exit code

  • Goal

  • Command

This lets you search and reuse good command ideas:

grep -i "journal" .agent_logs/history.tsv | cut -f4 | sort -u

Safety, tuning, and tips

  • Keep the allowlist tight. Add tools intentionally as you gain confidence.

  • Prefer a temp or sandbox directory for exploratory runs.

  • Local vs cloud: For sensitive logs, use Ollama locally. For tougher reasoning, point to a cloud model by exporting OPENAI_API_KEY.

  • Determinism: The script uses a low temperature for more repeatable outputs. You can adjust via the provider payload.

  • Troubleshooting:

    • Ensure Ollama is serving: nc -z 127.0.0.1 11434 should succeed.
    • Pull a compatible model: ollama pull mistral or ollama pull llama3.
    • Verify jq and curl are installed (see package manager commands above).

Bonus: Package manager installs recap

If you skipped above, here are the core dependencies again.

  • Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y curl jq ripgrep
  • Fedora (dnf):
sudo dnf install -y curl jq ripgrep
  • openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y curl jq ripgrep

Ollama (vendor script):

curl -fsSL https://ollama.com/install.sh | sh
ollama serve
ollama pull mistral

Conclusion and next steps

You don’t need a heavyweight framework to get real value from AI agents. With Bash, curl, and jq, you can:

  • Ask an LLM to plan a safe command pipeline.

  • Keep control with allowlists and confirmations.

  • Log everything for auditability and reuse.

Next steps: 1) Save and run agent.sh against your own logs and data.
2) Expand the allowlist to include your trusted tools (kubectl get, docker ps, ps aux, etc.), staying read-only.
3) Add simple “tools” by letting the agent call named functions (e.g., tool_sysinfo) that echo text for the model to reason about.

If you build something cool—or tighten the safety model—share it. The shell has always been a glue layer; adding an LLM turns it into a powerful, conversational copilot for everyday ops.