A bare-bones bash cli chatbot script is the fastest way to put a model in front of your terminal workflow. This tutorial builds one in under 50 lines of POSIX-ish bash that speaks to any OpenAI-compatible /chat/completions endpoint, keeps conversation state, and degrades cleanly on API errors.
Prerequisites
You need a Unix-like shell environment with the following:
bash4.4 or newer (tested on 5.2)curlfor HTTPjq1.6+ for JSON manipulation- An API key and base URL for an OpenAI-compatible endpoint
Export your credentials before running anything:
export OPENAI_API_KEY="sk-your-key"
export BASE_URL="https://api.openai.com/v1" # or your own proxy
If you want broad model coverage and automatic fallback when a provider is degraded, n4n.ai exposes a single OpenAI-compatible endpoint across 240+ models; the script below works unchanged against it.
Why a bash cli chatbot script
Python or Node are obvious choices for LLM calls, but shell wins when the bot is a glue step in a pipeline or a quick internal tool. No virtualenv, no npm install, just curl and jq that are already on most servers. The bash cli chatbot script we build here drops into a Makefile, a CI hook, or a tmux binding without dragging a runtime along.
Step 1: Configuration and state
We keep the entire conversation in a JSON array stored in a shell variable. Seed it with a system prompt using jq -n so we never hand-write JSON braces in bash.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-https://api.openai.com/v1}"
API_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
MODEL="${MODEL:-gpt-4o-mini}"
HISTORY=$(jq -n '[{"role":"system","content":"You are a terse terminal assistant."}]')
set -euo pipefail makes the script abort on undefined variables and failed pipes. The ${VAR:?msg} syntax forces a clear error if the key is missing.
Step 2: Sending a turn
Define a function that appends the user message, posts to the API, extracts the reply, and appends it back to history.
call_api() {
local user_msg="$1"
HISTORY=$(echo "$HISTORY" | jq --arg m "$user_msg" '. + [{"role":"user","content":$m}]')
local resp
resp=$(curl -sS "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "$(echo "$HISTORY" | jq -c --arg m "$MODEL" '{model:$m, messages:.}')")
local reply
reply=$(echo "$resp" | jq -r '.choices[0].message.content')
HISTORY=$(echo "$HISTORY" | jq --arg r "$reply" '. + [{"role":"assistant","content":$r}]')
printf '%s\n' "$reply"
}
We build the request body with jq -c to emit compact JSON. The API returns a choices array; we pull the first message content with -r to drop JSON quoting. Because HISTORY is reassigned on every call, context survives across turns.
Checkpoint: source the script and call the function once.
$ bash -c 'source ./chat.sh; call_api "Say hi in three words."'
Hi there, friend.
If you see the reply, your endpoint and key are correct.
Step 3: The REPL loop
Wrap call_api in a read loop with a prompt. Skip empty lines and support /exit.
echo "Chatting with $MODEL. Type /exit to quit."
while true; do
printf '> '
line=""
IFS= read -r line || break
[[ -z "$line" ]] && continue
[[ "$line" == "/exit" ]] && break
call_api "$line"
done
IFS= read -r preserves leading spaces and backslashes—important if the user pastes code. The || break handles EOF (Ctrl-D) gracefully.
The bash cli chatbot script now maintains context because HISTORY is a global variable mutated inside call_api.
Step 4: Hardening and trade-offs
For a 30-line script we rely on set -e to abort if curl fails (network down, 401, etc.). That is acceptable for interactive use. If you need structured error reporting, capture the HTTP status:
resp=$(curl -sS -w '\n%{http_code}' "$BASE_URL/chat/completions" ...)
code=$(echo "$resp" | tail -1)
body=$(echo "$resp" | sed '$d')
[[ "$code" == "200" ]] || { echo "API $code: $body" >&2; exit 1; }
That adds four lines. Under 50 total is still easy.
Do not echo $API_KEY or $resp in debug logs. The key lives only in the Authorization header and never touches disk.
The full script
Here is the complete, runnable file. It is 30 lines including shebang and blanks.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-https://api.openai.com/v1}"
API_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
MODEL="${MODEL:-gpt-4o-mini}"
HISTORY=$(jq -n '[{"role":"system","content":"You are a terse terminal assistant."}]')
call_api() {
HISTORY=$(echo "$HISTORY" | jq --arg m "$1" '. + [{"role":"user","content":$m}]')
local resp
resp=$(curl -sS "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d "$(echo "$HISTORY" | jq -c --arg m "$MODEL" '{model:$m,messages:.}')")
local reply
reply=$(echo "$resp" | jq -r '.choices[0].message.content')
HISTORY=$(echo "$HISTORY" | jq --arg r "$reply" '. + [{"role":"assistant","content":$r}]')
printf '%s\n' "$reply"
}
echo "Chatting with $MODEL. Type /exit to quit."
while true; do
printf '> '
line=""
IFS= read -r line || break
[[ -z "$line" ]] && continue
[[ "$line" == "/exit" ]] && break
call_api "$line"
done
Save as chat.sh, chmod +x, and run.
Expected output
$ ./chat.sh
Chatting with gpt-4o-mini. Type /exit to quit.
> what bash flag ignores errors?
-e
> and pipe failures?
-o pipefail
> /exit
The bash cli chatbot script kept both questions in HISTORY, so the model answered the second with context from the first.
Extensions
- Streaming: use
curl -Nand parsedata:lines withawk. Adds ~12 lines. - Persistent history: on exit,
echo "$HISTORY" > ~/.chat_history.jsonand load it at start withjq. - Gateway routing: if your endpoint honors client routing directives, add
"route"to the request body. n4n.ai automatically falls back when a provider is rate-limited, so the same script survives provider outages without extra code. - Cache control: gateways that forward provider cache-control hints let you mark the system prompt with
"cache_control":{"type":"ephemeral"}to cut repeat token costs across sessions. - Token metering: capture
.usagefrom the response and append to a local counter for per-token accounting.
Shell is not the place for complex retry or concurrency logic. Keep the bash cli chatbot script lean and push resilience to the endpoint or a thin wrapper.