- Posted on
- • Artificial Intelligence
AI Agents with Local LLMs
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
AI Agents with Local LLMs: Build Useful Bash Agents on Your Linux Box
If you’ve ever wished your shell could think, plan, and automate tasks without sending your data to the cloud, this is your moment. Local LLMs (large language models you run on your own machine) have become fast, affordable, and surprisingly capable. Pair them with a few Unix tools and you can build AI agents that search logs, summarize web pages, inspect system health, and more—entirely offline, with privacy by default.
This post shows you why local agents are worth your time and walks you through a minimal, hardenable Bash agent you can run today.
Why local AI agents are worth it
Privacy and control: Keep code, logs, and credentials on your machine. No API keys, no egress.
Cost and predictability: No per-token fees. Your hardware, your schedule.
Latency and reliability: Works offline. Snappy responses for day-to-day ops work.
Linux synergy: LLMs shine when they can call shell tools—grep, curl, awk, journalctl—exactly what Linux does best.
What you’ll build
A tiny Bash agent that talks to a local LLM
A safe, allowlisted tool-calling loop (search files, fetch a URL, run a few system commands)
Real-world examples, plus simple hardening tips
1) Install a local LLM runtime
The easiest path is Ollama, which runs models locally and exposes a simple HTTP API. Optionally, you can build llama.cpp from source.
Option A: Ollama (recommended)
Install prerequisites if needed.
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y curl jq ripgrep
- Fedora/RHEL (dnf):
sudo dnf install -y curl jq ripgrep
- openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y curl jq ripgrep
Install Ollama:
curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl enable --now ollama
Pull a capable, compact model (you can change later):
ollama pull llama3.1:8b
Confirm it works:
curl -s http://localhost:11434/api/version
Option B: llama.cpp (advanced, from source)
Install build tools and git.
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y git build-essential cmake
- Fedora/RHEL (dnf):
sudo dnf install -y git @development-tools cmake
- openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y git gcc gcc-c++ make cmake
Build:
git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
make -j
Download a GGUF model (example uses TheBloke’s mirrors or official model sources) and start the HTTP server:
# Example: place your *.gguf model in ./models and start the server
./server -m ./models/YourModel.gguf -c 4096 --port 8080
If you use llama.cpp server, just swap the API URL in the scripts below.
2) Give your agent some tools
We’ll use jq for JSON, ripgrep for fast search, and curl for HTTP. If you haven’t already:
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y curl jq ripgrep
- Fedora/RHEL (dnf):
sudo dnf install -y curl jq ripgrep
- openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y curl jq ripgrep
Optional niceties: bat, fd, html2text, or lynx to pretty-print and de-HTML content.
3) A minimal, safe Bash agent
This script implements a small “tool calling” loop. The model chooses an action by emitting JSON. We validate and run only allowlisted commands, feed the result back, and let the model finish with a final answer.
Save as agent.sh and make executable.
#!/usr/bin/env bash
set -euo pipefail
# Config
MODEL="${MODEL:-llama3.1:8b}"
API="${API:-http://localhost:11434/api/chat}" # Ollama chat endpoint
MAX_STEPS="${MAX_STEPS:-3}"
TMP_DIR="${TMP_DIR:-/tmp/local-agent}"
mkdir -p "$TMP_DIR"
# Allowlisted commands for 'run_cmd'
ALLOWED_CMDS=("uptime" "df" "free" "uname")
# Keep outputs to a sane size
MAX_TOOL_BYTES=20000
json_escape() {
# Escape for JSON string context
jq -Rs .
}
allow_run_cmd() {
local name="$1"; shift || true
for c in "${ALLOWED_CMDS[@]}"; do
if [[ "$name" == "$c" ]]; then return 0; fi
done
return 1
}
tool_search_files() {
local pattern="$1"
local path="${2:-.}"
local context="${3:-1}"
rg --no-messages -n -C "$context" -S "$pattern" "$path" | head -c "$MAX_TOOL_BYTES" || true
}
tool_web_get() {
local url="$1"
curl -fsSL --max-time 10 "$url" | head -c "$MAX_TOOL_BYTES" || true
}
tool_run_cmd() {
local name="$1"; shift || true
if ! allow_run_cmd "$name"; then
echo "Command not allowed: $name"; return 1
fi
# Ensure args are treated as separate words; disallow dangerous characters
local safe_args=()
for a in "$@"; do
if [[ "$a" =~ [\;\|\&\>\\`] ]]; then
echo "Blocked unsafe arg: $a"; return 1
fi
safe_args+=("$a")
done
"$name" "${safe_args[@]}" 2>&1 | head -c "$MAX_TOOL_BYTES"
}
system_prompt='You are a careful Linux command-line AI agent.
You can choose one of these actions:
- "search_files": args {"pattern": "...", "path": "/path/optional", "context": 1-5}
- "web_get": args {"url": "https://..."}
- "run_cmd": args {"name": "uptime|df|free|uname", "args": ["...optional..."]}
- "final": args {"answer": "final user-facing answer"}
Rules:
- Always respond with a single compact JSON object: {"action":"...","args":{...},"explanation":"short reason"}.
- If you need more info from a tool, choose a tool action. When enough, choose "final".
- Keep outputs small and focused.'
call_llm() {
local messages_json="$1"
curl -s "$API" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg model "$MODEL" --argjson messages "$messages_json" \
'{model:$model, messages:$messages, stream:false}')" \
| jq -r '.message.content'
}
run_agent() {
local user_input="$1"
# Build message history
local messages
messages=$(jq -n --arg sys "$system_prompt" --arg user "$user_input" \
'[{"role":"system","content":$sys},{"role":"user","content":$user}]')
for (( step=1; step<=MAX_STEPS; step++ )); do
local content
content="$(call_llm "$messages")"
# Try to parse assistant content as JSON
if ! echo "$content" | jq -e . >/dev/null 2>&1; then
# If the model didn't return JSON, wrap it as final
echo "$content"
return 0
fi
local action
action="$(echo "$content" | jq -r '.action // empty')"
case "$action" in
search_files)
local pattern path context
pattern="$(echo "$content" | jq -r '.args.pattern // ""')"
path="$(echo "$content" | jq -r '.args.path // "."')"
context="$(echo "$content" | jq -r '.args.context // 1')"
out="$(tool_search_files "$pattern" "$path" "$context" | head -c "$MAX_TOOL_BYTES")"
;;
web_get)
local url
url="$(echo "$content" | jq -r '.args.url // ""')"
out="$(tool_web_get "$url" | head -c "$MAX_TOOL_BYTES")"
;;
run_cmd)
local name args_json
name="$(echo "$content" | jq -r '.args.name // ""')"
mapfile -t args < <(echo "$content" | jq -r '.args.args // [] | .[]')
if out="$(tool_run_cmd "$name" "${args[@]}" | head -c "$MAX_TOOL_BYTES")"; then
:
else
out="Tool error or blocked."
fi
;;
final)
echo "$(echo "$content" | jq -r '.args.answer // empty')"
return 0
;;
*)
echo "Unknown or missing action. Model said:"
echo "$content"
return 1
;;
esac
# Append tool result back to the conversation
# We label it as a user message with a clear prefix; the system prompt already explains this pattern.
local tool_msg="TOOL_RESULT:
$(echo "$out")"
messages="$(echo "$messages" | jq --arg c "$content" --arg t "$tool_msg" \
'. + [{"role":"assistant","content":$c},{"role":"user","content":$t}]')"
done
echo "Reached step limit without final answer."
return 1
}
if [[ "${1-}" == "--demo" ]]; then
run_agent "Find recent errors in /var/log and summarize top issues."
else
if [[ $# -eq 0 ]]; then
echo "Usage: $0 \"your question or task\""
echo "Example: $0 \"Check disk space and tell me if any partition is >80% full\""
exit 1
fi
run_agent "$*"
fi
Usage:
chmod +x agent.sh
./agent.sh "Search /var/log for 'error' and summarize the top 3 recurring messages"
./agent.sh "Fetch https://www.kernel.org and summarize the latest stable release"
./agent.sh "Run df -h and tell me which partitions exceed 80% usage"
Notes:
This is a minimal loop for clarity. You can extend it with more tools, e.g.,
journalctl,ps,ss,kubectl, etc.Never run as root. Keep a tight allowlist. Consider running in a dedicated, unprivileged user or container/VM if you expand capabilities.
4) Real-world examples you can try
Log triage (offline):
- Prompt: “Scan /var/log for ‘failed’ and summarize probable causes and remediation steps.”
- The agent uses search_files, returns context snips, then finalizes with a concise summary.
Quick system health:
- Prompt: “Run ‘uptime’, ‘free -h’, and ‘df -h’. Are we constrained on CPU, RAM, or disk? Give recommendations.”
- The agent uses run_cmd on allowlisted commands and provides an actionable conclusion.
Web note-taking:
- Prompt: “Fetch https://www.openssh.com/releasenotes.html and summarize the key security fixes in 10 bullets.”
- The agent uses web_get, then condenses to a readable summary. Add html-to-text if you want richer parsing.
Focused code search:
- Prompt: “In ./src, find references to TLS version settings and list files + context that touch TLS 1.0.”
- The agent calls search_files with a pattern and path, returns context blocks for quick refactors.
5) Hardening and performance tips
Least privilege:
- Run as a non-root user.
- Keep a strict allowlist for commands and validate args.
- Add timeouts: prepend
timeout 10sto risky tools. - Limit output: the script already truncates tool outputs.
Sandbox:
- Consider Firejail, containers, or a VM if you add file-write or network-heavy tools.
Model choice:
- Start with 7–8B models for speed on laptops; move to 13B for better reasoning if you have RAM.
- Quantized models (e.g., Q4_K_M) run faster with smaller footprints.
Caching and context:
- Keep prompts short and focused.
- For repeated tasks, store context snippets in files and feed them explicitly.
Swap runtimes easily:
- If using llama.cpp server on port 8080, set:
API=http://localhost:8080/v1/chat/completionsif using its OpenAI-compatible endpoint- Adjust the request body accordingly or add a small adapter function.
Troubleshooting
- Ollama not responding:
systemctl status ollama
journalctl -u ollama -e
Large outputs clipped:
- Increase
MAX_TOOL_BYTESat the top of the script (be cautious).
- Increase
Model replies not JSON:
- Models sometimes drift. Tighten the system prompt or add a lightweight “repair” step that extracts JSON via
jq -Randjq -r.
- Models sometimes drift. Tighten the system prompt or add a lightweight “repair” step that extracts JSON via
Conclusion and next steps
You now have a working, private AI agent running entirely on Linux with a few dozen lines of Bash. From here:
Add new safe tools for your environment:
journalctl,ps,ss,kubectl,terraform,aws s3 ls, etc.Create task-specific wrappers:
logs-agent.sh,cluster-agent.sh,sec-audit-agent.sh.Experiment with different models in Ollama:
mistral,llama3.1:8b-instruct, or domain-specific variants.
Call to action:
- Install Ollama, drop the
agent.shscript into your bin, and ask it to investigate your logs right now. Then iterate—one tool at a time—until it becomes the teammate you always wanted, running locally under your control.