- Posted on
- • Artificial Intelligence
Artificial Intelligence Agent Best Practices
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
AI Agents from the Shell: Best Practices for Reliable, Safe, and Reproducible Automation
AI agents promise to turn messy, multi-step tasks into one command. But without guardrails, they can run up your bill, leak secrets, or simply fail in surprising ways. The good news: the Linux command line gives you everything you need to build agents that are safe, observable, and repeatable—no heavy frameworks required.
This post explains why best practices for AI agents matter and gives you a handful of practical Bash patterns you can paste into your own scripts today.
Why this matters (and why Bash is perfect for it)
Reliability beats novelty. Many “autonomous” agents look magical in demos but crumble under rate limits, flaky networks, or ambiguous outputs. Bash’s discipline—idempotency, composability, small tools—helps.
Safety is non‑negotiable. Least-privilege execution, clear allowlists, and sandboxes are standard practice on Linux. Your agents should follow suit.
Reproducibility wins. With simple caches, logs, and tests, you can rerun an agent tomorrow and get the same outcome, audit what happened, and improve with confidence.
Prerequisites: Install the basics
These examples use a few standard CLI tools. Install them with your distro’s package manager.
curl (HTTP client)
jq (JSON processor)
podman (container sandbox; rootless by default)
firejail (namespace/seccomp sandbox)
bats (Bash tests; optional but recommended)
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y curl jq podman firejail bats
Fedora/RHEL (dnf):
sudo dnf install -y curl jq podman firejail bats
openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y curl jq podman firejail bats
Note: If any package isn’t available on your distro version, install the others and proceed; you can swap podman/firejail for the one you have.
1) Isolate by default: run agent tools with least privilege
Agents that can “call tools” should run in a sandbox with no network and minimal filesystem access. Two easy options:
Firejail (fast, no images to pull)
Podman (rootless containers with strong isolation)
Example: allowlisted commands executed in a sandboxed workspace.
#!/usr/bin/env bash
set -euo pipefail
WORKDIR="${WORKDIR:-$PWD/work}"
mkdir -p "$WORKDIR"
# Allowlist of safe commands for the agent to run
is_allowed_cmd() {
case "$1" in
grep|sed|awk|cut|sort|uniq|wc|head|tail|cat|tr) return 0 ;;
*) return 1 ;;
esac
}
safe_run_firejail() {
local cmd="$1"; shift
is_allowed_cmd "$cmd" || { echo "Denied: $cmd"; return 1; }
firejail --quiet --net=none --private="$WORKDIR" -- bash -lc "$cmd \"\$@\"" _ "$@"
}
safe_run_podman() {
local cmd="$1"; shift
is_allowed_cmd "$cmd" || { echo "Denied: $cmd"; return 1; }
podman run --rm --network=none --read-only \
-v "$WORKDIR":/work:Z -w /work docker.io/library/alpine:latest \
sh -lc "$cmd \"\$@\"" _ "$@"
}
# Example usage (pick one):
safe_run_firejail grep -R "TODO" .
# or
# safe_run_podman grep -R "TODO" .
What this does:
Allowlists only harmless text utilities.
Firejail: no network, private per-task directory.
Podman: no network, read-only rootfs, bind-mount a single working dir.
2) Make calls idempotent with a cheap content-addressed cache
Agents often repeat calls with the same prompt/context. Cache results by hashing inputs and saving the raw JSON.
#!/usr/bin/env bash
set -euo pipefail
CACHE_DIR="${CACHE_DIR:-.ai-cache}"
mkdir -p "$CACHE_DIR"
ai_hash() {
# Hash the request-defining inputs (model, endpoint path, prompt, tools signature)
printf '%s\0' "$AI_BASE_URL" "$AI_MODEL" "$1" "${2:-}" | sha256sum | awk '{print $1}'
}
ai_request() {
local prompt="$1"
local tools_sig="${2:-}" # include a stable signature if tools/context change
local key; key="$(ai_hash "$prompt" "$tools_sig")"
local cache="$CACHE_DIR/$key.json"
if [[ -f "$cache" ]]; then
cat "$cache"
return 0
fi
# Backoff/retry on transient failures
local attempt=0 max=4 delay=1
while (( attempt < max )); do
if response="$(curl -sS --fail-with-body \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${AI_API_KEY:-}" \
--max-time 30 \
"$AI_BASE_URL/responses" \
-d "$(jq -n --arg m "$AI_MODEL" --arg p "$prompt" '{model:$m,input:$p}')" )"; then
printf '%s' "$response" > "$cache"
printf '%s\n' "$response"
return 0
fi
attempt=$((attempt+1))
sleep "$delay"; delay=$((delay*2))
done
echo "Request failed after retries" >&2
return 1
}
Benefits:
Deterministic retries that don’t blow past your usage limits.
Easy to purge or pin results by deleting files in
.ai-cache/.
3) Log everything (structured!), then analyze with jq
Text logs are nice; JSONL is better. Keep request/response summaries for auditability and tuning.
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="${LOG_DIR:-logs}"
mkdir -p "$LOG_DIR"
log_event() {
local type="$1" msg="$2"
jq -nc --arg t "$(date -Is --utc)" --arg type "$type" --arg msg "$msg" \
'{ts:$t,type:$type,msg:$msg}' >> "$LOG_DIR/agent.jsonl"
}
# Example: wrap ai_request to add logs
ai_ask() {
local prompt="$1"
log_event "request" "model=$AI_MODEL len=${#prompt}"
local resp
if ! resp="$(ai_request "$prompt")"; then
log_event "error" "request_failed"
return 1
fi
log_event "response" "bytes=${#resp}"
printf '%s\n' "$resp"
}
You can filter later:
jq -r 'select(.type=="error")' logs/agent.jsonl
Mask secrets by never logging auth headers or env vars.
4) Parse model output defensively
Models differ in response shapes. Extract the “text” in a tolerant way and fail safe if it’s missing.
extract_text() {
jq -r '
.output[0].content[0].text? //
.choices[0].message.content? //
.message?.content? //
.text? //
empty
'
}
# Usage
ai_ask "Summarize this log:" | extract_text || {
echo "No text content found in response" >&2
exit 1
}
This avoids brittle scripts that break when you swap providers or models.
5) Test before trust: add a tiny Bats test
Even one or two tests can catch regressions when you tweak prompts or upgrade dependencies.
Create test/extract_text.bats:
#!/usr/bin/env bats
@test "extract_text handles common shapes" {
run bash -lc 'jq -n "{choices:[{message:{content:\"hello\"}}]}" | '"'"'extract_text'"'"
[ "$status" -eq 0 ]
[ "$output" = "hello" ]
}
Run tests:
bats test
If you haven’t yet:
Debian/Ubuntu:
sudo apt install -y batsFedora/RHEL:
sudo dnf install -y batsopenSUSE:
sudo zypper install -y bats
Putting it together: a minimal .env and starter script
.env (do not commit this):
AI_BASE_URL=https://api.example.ai/v1
AI_API_KEY=sk-your-key
AI_MODEL=example-model
agent.sh:
#!/usr/bin/env bash
set -euo pipefail
# Load env safely
set -a
[ -f .env ] && . ./.env
set +a
source ./lib_cache.sh
source ./lib_logs.sh
source ./lib_sandbox.sh
source ./lib_extract.sh
prompt="List unique error messages from ./logs/app.log"
safe_run_firejail grep -oE "ERROR:.*" ./logs/app.log | sort | uniq > work/errors.txt
response_json="$(ai_ask "Summarize these errors:\n$(cat work/errors.txt)")"
summary="$(printf '%s' "$response_json" | extract_text)"
printf 'Summary:\n%s\n' "$summary"
This pattern:
Prepares data with allowlisted tools in a sandbox.
Calls the model with caching, retries, and logs.
Extracts text defensively.
Conclusion and next steps
You don’t need a monolithic “agent framework” to build reliable AI automation. The Unix toolbox already gives you:
Isolation (firejail, podman)
Idempotency (content-addressed caching)
Observability (JSONL logs + jq)
Safety (allowlists and fail-safe parsing)
Confidence (Bats tests)
Your next step: 1) Install the tools via your package manager. 2) Copy the snippets above into small, focused libs (cache, logs, sandbox, extract). 3) Start with a single agent task you run weekly—turn it into a safe, cached, logged script. 4) Iterate: add more tools to the allowlist only when needed, and test as you go.
If you want a ready-to-fork starter, drop these snippets into a repo with a Makefile target for test, run, and clean. Then grow your agent’s capabilities one guarded command at a time.