Posted on
Artificial Intelligence

Building a Linux Artificial Intelligence Agent Using Bash

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

Building a Linux Artificial Intelligence Agent Using Bash

What if your terminal could think with you? Imagine asking your shell to draft scripts, summarize logs, or suggest safe commands—and having it respond like a power-user coworker. In this article, we’ll build a small-but-capable AI agent using plain Bash, curl, and jq that works with either a cloud LLM (OpenAI) or a local model (Ollama). No heavy frameworks. Fully transparent. Yours to tweak.

Why build an AI agent in Bash?

  • Bash is the glue of Linux. It’s easy to wire the model’s reasoning to real tools: grep, find, journalctl, curl, etc.

  • It’s auditable. You can log prompts, responses, and executed commands. No black boxes.

  • It’s portable. Works across distros and shells with minimal dependencies.

  • It’s adaptable. Swap providers (cloud or local) with a single variable.

Below you’ll find step-by-step instructions, complete code, and hardening tips so you can put an AI assistant to work securely on your workstation or server.


Prerequisites

We’ll need curl and jq. Install them with your package manager:

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

Optional but recommended:

  • A cloud LLM API key (e.g., OpenAI).

  • Or a local LLM runtime (Ollama) to run models on your machine.


Step 1: Pick your model backend

You can run this agent against:

  • A cloud LLM via HTTP (example: OpenAI).

  • A local server via Ollama (no internet required once the model is pulled).

Option A — OpenAI (cloud): 1) Export your API key:

export OPENAI_API_KEY="sk-...your-key..."

2) Pick a model (example: gpt-4o-mini). You can change this later in the script.

Option B — Ollama (local): 1) Install Ollama:

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

2) Start the service (it usually starts automatically) and pull a model:

ollama serve  # in one terminal, if not already running
ollama pull llama3.1

3) Confirm the server responds:

curl -s http://localhost:11434/api/tags | jq

Notes:

  • On constrained hardware, consider smaller models (e.g., q4_0 variants in Ollama).

  • You can switch providers anytime by changing a variable.


Step 2: Create a minimal Bash AI agent

This script maintains a conversation, calls your chosen model, and prints the assistant’s reply. It also supports an optional “execute this command” pattern with a human-in-the-loop confirmation.

Save as agent.sh and make it executable:

chmod +x agent.sh

agent.sh:

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

# Configuration
: "${PROVIDER:=ollama}"     # "ollama" or "openai"
: "${MODEL:=llama3.1}"      # e.g., "llama3.1" (Ollama) or "gpt-4o-mini" (OpenAI)
: "${MAX_TIME:=120}"        # curl request timeout (seconds)
: "${WORKDIR:=$(pwd)}"
: "${MSG_FILE:=$WORKDIR/messages.json}"
: "${LOG_FILE:=$WORKDIR/chat.jsonl}"  # JSONL for easy audit/logging

# Sanity checks
command -v curl >/dev/null || { echo "curl is required."; exit 1; }
command -v jq >/dev/null || { echo "jq is required."; exit 1; }

if [[ "$PROVIDER" == "openai" ]]; then
  : "${OPENAI_API_KEY:?Set OPENAI_API_KEY for OpenAI provider}"
fi

init_messages() {
  if [[ ! -f "$MSG_FILE" ]]; then
    printf '%s\n' '[
      {
        "role": "system",
        "content": "You are a cautious, helpful Linux assistant. Be concise. When a shell command would help, include a single line starting with >> RUN: followed by a safe, self-contained command. Never use sudo. Prefer read-only or dry-run alternatives when possible."
      }
    ]' > "$MSG_FILE"
  fi
}

append_message() {
  local role="$1"
  local content="$2"
  # Append a message object to the messages array
  tmp="$(mktemp)"
  jq --arg role "$role" --arg content "$content" '. + [{"role": $role, "content": $content}]' "$MSG_FILE" > "$tmp"
  mv "$tmp" "$MSG_FILE"
}

log_event() {
  # Log events to JSONL: {"ts": "...", "role": "...", "content": "..."}
  local role="$1"
  local content="$2"
  local ts
  ts="$(date -Is)"
  jq -n --arg ts "$ts" --arg role "$role" --arg content "$content" \
     '{ts: $ts, role: $role, content: $content}' >> "$LOG_FILE"
}

query_model_openai() {
  curl -sS --max-time "$MAX_TIME" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer '"$OPENAI_API_KEY"'" \
    -d '{
      "model": "'"$MODEL"'",
      "messages": '"$(cat "$MSG_FILE")"',
      "temperature": 0.2
    }' \
    https://api.openai.com/v1/chat/completions
}

query_model_ollama() {
  curl -sS --max-time "$MAX_TIME" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "'"$MODEL"'",
      "messages": '"$(cat "$MSG_FILE")"',
      "stream": false
    }' \
    http://localhost:11434/api/chat
}

query_model() {
  if [[ "$PROVIDER" == "openai" ]]; then
    query_model_openai
  else
    query_model_ollama
  fi
}

extract_assistant_content() {
  local response="$1"
  if [[ "$PROVIDER" == "openai" ]]; then
    jq -r '.choices[0].message.content // ""' <<< "$response"
  else
    jq -r '.message.content // ""' <<< "$response"
  fi
}

confirm_and_run() {
  local cmd="$1"

  # Basic guardrails
  if grep -Eq '(^|[[:space:]])sudo([[:space:]]|$)' <<< "$cmd"; then
    echo "Blocked: command contains sudo."
    return 1
  fi
  if grep -Eq 'rm[[:space:]]+-rf[[:space:]]+/($|[[:space:]])' <<< "$cmd"; then
    echo "Blocked: dangerous rm -rf / pattern."
    return 1
  fi

  echo
  echo "The assistant suggests running:"
  echo "  $cmd"
  read -r -p "Run this command? [y/N] " ans
  if [[ "${ans,,}" != "y" ]]; then
    echo "Skipped."
    return 1
  fi

  # Execute in a restricted subshell (no sudo) and capture output and exit code
  echo
  echo "Running..."
  set +e
  output="$(bash -lc "$cmd" 2>&1)"
  ec=$?
  set -e

  echo "Exit code: $ec"
  echo "----- command output -----"
  echo "$output"
  echo "--------------------------"

  # Instead of using a special role, pass results back as a user message
  append_message "user" "Execution result (exit=$ec):\n$output"
  log_event "tool" "cmd: $cmd\nexit: $ec\n$output"

  return 0
}

main_loop() {
  init_messages
  echo "AI agent ready. Provider=$PROVIDER, Model=$MODEL"
  echo "Type your request. Ctrl+C to quit."
  while true; do
    echo
    read -r -p "You> " user_input || break
    [[ -z "$user_input" ]] && continue

    append_message "user" "$user_input"
    log_event "user" "$user_input"

    resp="$(query_model)"
    assistant="$(extract_assistant_content "$resp")" || assistant=""

    if [[ -z "$assistant" ]]; then
      echo "Assistant> [no content returned]"
      continue
    fi

    echo
    echo "Assistant> $assistant"
    append_message "assistant" "$assistant"
    log_event "assistant" "$assistant"

    # Look for a suggested command line of the form:
    # >> RUN: <command>
    if run_line="$(grep -oE '^>>[[:space:]]*RUN:[[:space:]].*$' <<< "$assistant" | head -n1)"; then
      cmd="${run_line#*RUN: }"
      confirm_and_run "$cmd" || true

      # After running (or skipping), ask the model to summarize next steps
      # Only if we actually appended an execution result
      if tail -n1 "$LOG_FILE" | grep -q '"role": "tool"'; then
        followup="Given the execution result above, provide a concise next step or final answer."
        append_message "user" "$followup"
        log_event "user" "$followup"

        resp2="$(query_model)"
        assistant2="$(extract_assistant_content "$resp2")" || assistant2=""
        if [[ -n "$assistant2" ]]; then
          echo
          echo "Assistant> $assistant2"
          append_message "assistant" "$assistant2"
          log_event "assistant" "$assistant2"
        fi
      fi
    fi
  done
}

trap 'echo; echo "Exiting."; exit 0' INT
main_loop

How it works:

  • The script keeps a JSON array of messages in messages.json.

  • It calls either OpenAI or Ollama with curl and parses the assistant’s content with jq.

  • If the assistant proposes a command using the pattern “>> RUN: …”, you get a safety prompt before executing it.

  • All traffic is logged to chat.jsonl for auditing.

Switch providers at runtime:

PROVIDER=openai MODEL=gpt-4o-mini ./agent.sh
# or
PROVIDER=ollama MODEL=llama3.1 ./agent.sh

Step 3: Try real-world tasks

Here are three practical things your Bash agent can do.

1) Summarize a noisy log

  • Ask the agent:
Summarize the most frequent error patterns from /var/log/syslog in 20 words.
If a command helps, propose it.
  • The assistant might suggest:
>> RUN: grep -i 'error' /var/log/syslog | awk -F':' '{print $NF}' | sed 's/^[[:space:]]*//' | sort | uniq -c | sort -nr | head -20
  • Confirm to run, then the agent will interpret the output.

2) Draft or review a shell script

  • Ask:
Write a Bash script that finds the top 10 largest directories under /var/log and prints their sizes, excluding rotated archives. Prefer portable tools.
  • The agent will propose a script or a command pipeline. You can paste it into a file, or ask for a “>> RUN:” dry-run to verify first.

3) Safe grep across code

  • Ask:
I need to find where we call curl with -k in this repo. Propose a safe read-only command.
  • Expect something like:
>> RUN: grep -RIn --exclude-dir=.git -- 'curl .* -k' .

Tip: If you want more conservative behavior, tell the agent in your first prompt, e.g., “Never propose commands that modify files; only list or print.”


Step 4: Hardening and quality-of-life tweaks

  • Timeouts: MAX_TIME caps network calls so a bad request doesn’t hang your terminal.

  • Guardrails: The script blocks sudo and a dangerous rm -rf pattern. Extend this list to your needs.

  • Confirmations: You must explicitly confirm before any suggested command executes.

  • Dry runs: Ask the agent to add flags like -n, --dry-run, or --diff where available.

  • Logging: chat.jsonl keeps a timestamped trail of prompts, responses, and executed commands.

  • Non-interactive use: Pipe a one-off question via printf or echo and patch the script to read from stdin if needed.

  • Multiple profiles: Make copies with different system prompts, e.g., one focused on ops, one on data munging.


Troubleshooting

  • jq: command not found

    • Install with your package manager:
    • apt: sudo apt install -y jq
    • dnf: sudo dnf install -y jq
    • zypper: sudo zypper install -y jq
  • OpenAI 401 Unauthorized

    • Ensure export OPENAI_API_KEY=...
    • Verify your account has access to the chosen model.
  • Ollama connection refused

    • Start the server: ollama serve
    • Confirm endpoint: curl -s http://localhost:11434/api/tags | jq
    • Pull and use a valid model name: ollama pull llama3.1
  • Responses are empty or low-quality

    • Try a different model.
    • Adjust the system prompt for stricter instructions.
    • Set temperature to 0–0.3 for more deterministic answers.

Conclusion and next steps

You now have a transparent, auditable AI agent that runs straight from Bash. It can analyze logs, draft commands, and collaborate with you—without hiding how it works.

Where to go from here:

  • Add domain tools: ripgrep, fd, bat, jq recipes, man-page summarization.

  • Teach structured tool use: make the model output JSON tool requests and parse them in Bash.

  • Add voice I/O: pipe microphone input through a speech-to-text CLI and speak responses with say/espeak.

  • Schedule routines: run the agent in cron to summarize system health and email a daily report.

Clone the script, tweak the system prompt to your environment, and make your terminal a thinking partner.