Using LLMs from the Bash Command Line: llm, aichat, and Shell Pipelines

Why Use LLMs from the Terminal?

Opening a browser tab to ChatGPT mid-workflow is a context switch that kills momentum. If you're already in a terminal editing code, analyzing logs, or writing scripts, the last thing you need is a GUI. LLMs accessible directly from bash mean you can pipe output from any command straight into an AI model, embed inference in automation scripts, and build reusable shell functions that would otherwise require custom applications.

The practical wins are significant: summarize a 500-line error log in seconds, generate a commit message from git diff, explain an unfamiliar command, or batch-process hundreds of files with AI assistance — all without leaving the shell. The tools covered here treat LLMs as Unix citizens: they read from stdin, write to stdout, and compose naturally with grep, jq, awk, and fzf.

Installing and Configuring llm by Simon Willison

llm is a Python CLI tool and library built by Simon Willison that provides a unified interface to dozens of LLM providers. It supports OpenAI, Anthropic, Google Gemini, Mistral, and local models via plugins.

Installation

# Install via pip (use pipx to keep it isolated)
pip install llm

# Recommended: use pipx for clean isolation
pipx install llm

# Verify installation
llm --version

Configuring API Keys

# Set your OpenAI key (stored securely in system keyring)
llm keys set openai
# You'll be prompted: Enter key: sk-...

# Set Anthropic key
llm keys set anthropic

# View stored keys (names only, not values)
llm keys

# Alternatively, use environment variables
export OPENAI_API_KEY="sk-your-key-here"
export ANTHROPIC_API_KEY="sk-ant-your-key-here"

Managing Models and Plugins

# List available models from installed providers
llm models

# Install the Anthropic plugin for Claude models
llm install llm-claude-3

# Install Google Gemini plugin
llm install llm-gemini

# Install plugin for local Ollama models
llm install llm-ollama

# List all installed plugins
llm plugins

# Set a default model
llm models default gpt-4o-mini

Basic llm Usage: Prompts, Piping, and Flags

Simple Prompts

# Basic prompt
llm "What is the difference between a hard link and a symbolic link?"

# Specify a model explicitly
llm -m claude-3-5-sonnet-latest "Explain eBPF in three sentences"

# Specify GPT-4o
llm -m gpt-4o "Write a Python one-liner to flatten a nested list"

Piping stdin

llm reads from stdin when you pipe data to it, which is where its real power emerges.

# Pipe a file's contents for analysis
cat config.yaml | llm "Identify any security misconfigurations in this config"

# Pipe command output
df -h | llm "Which filesystems are near capacity? Summarize in plain English"

# Pipe a Python file for code review
cat app.py | llm "Review this code for bugs and suggest improvements"

# Combine file contents and a prompt
llm "Summarize this log file and list all unique error types" < /var/log/syslog

The --system Flag

The -s or --system flag sets a system prompt, allowing you to define a persona or context before your user prompt arrives.

# Act as a strict security auditor
cat nginx.conf | llm -s "You are a security-focused sysadmin. Be terse and direct." 
  "Find vulnerabilities in this nginx configuration"

# Act as a senior code reviewer
cat main.go | llm -s "You are a senior Go developer. Focus on performance and idiomatic Go." 
  "Review this code"

# Force structured output
ps aux | llm -s "Always respond with valid JSON only, no prose." 
  "List the top 5 processes by CPU as a JSON array with pid, name, and cpu fields"

Continuing Conversations

# Start a conversation and get a conversation ID
llm "Explain how TCP handshakes work" --cid tcp-talk

# Continue the same conversation
llm --cid tcp-talk "Now explain how TLS builds on top of that"

# List recent conversations
llm logs list

# View a conversation's full log
llm logs show tcp-talk

Real Bash Pipeline Examples

Git Diff to Commit Message

# Generate a commit message from staged changes
git diff --cached | llm "Write a concise git commit message for these changes. 
Use conventional commits format (feat/fix/refactor/etc). 
Output only the commit message, nothing else."

# One-liner to commit with AI-generated message
git commit -m "$(git diff --cached | llm 'Write a git commit message in conventional commits format. Output only the message.')"

Error Log Analysis

# Analyze the last 100 lines of a log
tail -n 100 /var/log/nginx/error.log | llm "Summarize these nginx errors. 
Group by error type, note frequency, and suggest root causes."

# Find and explain errors from systemd
journalctl -u postgresql --since "1 hour ago" --no-pager | 
  llm "What errors occurred in PostgreSQL in the last hour and what likely caused them?"

# Extract and explain Python tracebacks
grep -A 20 "Traceback" app.log | llm "Explain each Python traceback and suggest a fix"

Summarizing curl Output

# Summarize an API response
curl -s https://api.github.com/repos/torvalds/linux/releases | 
  jq '.[0:5]' | 
  llm "Summarize these Linux kernel releases in plain English"

# Analyze HTTP headers for security
curl -sI https://example.com | llm "Analyze these HTTP headers for missing security headers"

# Summarize a webpage (using curl + html2text if installed)
curl -s https://news.ycombinator.com | 
  llm -s "Extract and summarize the top 5 Hacker News stories from this HTML. Output as a numbered list."

Saving and Reusing Templates with llm -t

Templates let you define system prompts and default settings once and reuse them by name.

# Create a template for commit messages
llm templates edit commit-msg

This opens your $EDITOR with a YAML template file. Populate it like this:

system: >
  You are an expert at writing git commit messages.
  Use conventional commits format (feat/fix/docs/refactor/test/chore).
  Be concise. Output only the commit message with no explanation.
model: gpt-4o-mini
prompt: Write a commit message for these changes:
# Use the template
git diff --cached | llm -t commit-msg

# Create a shell-explainer template
llm templates edit explain-shell
system: >
  You are a Linux expert. Explain shell commands clearly for intermediate users.
  Break down each part of the command. Keep it concise but thorough.
model: gpt-4o-mini
# Use the shell explainer
echo "find / -name '*.log' -mtime +30 -delete" | llm -t explain-shell

# List all templates
llm templates list

# View a template's content
llm templates show commit-msg

Installing and Using aichat

aichat is a Rust-based CLI tool that emphasizes interactive chat sessions, a built-in REPL, shell integration, and a rich roles system. It's faster to start up than Python-based tools and ships as a single binary.

Installation

# Install via cargo
cargo install aichat

# Or download the prebuilt binary
curl -sSfL https://raw.githubusercontent.com/sigoden/aichat/main/install.sh | sh

# On Arch Linux
yay -S aichat

# On macOS (for reference)
brew install aichat

Configuration

# First run creates config at ~/.config/aichat/config.yaml
aichat

# Edit config directly
cat ~/.config/aichat/config.yaml
model: openai:gpt-4o-mini
temperature: 0.7
save: true
highlight: true
clients:
  - type: openai
    api_key: sk-your-key-here
  - type: claude
    api_key: sk-ant-your-key

Basic aichat Usage

# Simple one-shot query
aichat "Explain the difference between epoll and select"

# Pipe input
cat Dockerfile | aichat "Optimize this Dockerfile for layer caching and security"

# Use a specific model
aichat -m claude:claude-3-5-sonnet-20241022 "Write a regex to match IPv6 addresses"

# Execute mode: generate and run a shell command
aichat -e "find all files larger than 100MB in /home"
# aichat will generate the command and ask for confirmation before running

# Code mode: output only code
aichat -c "Python function to retry a request with exponential backoff"

aichat Roles

# Create a role file at ~/.config/aichat/roles/devops.yaml
cat > ~/.config/aichat/roles/devops.yaml << 'EOF'
name: devops
prompt: >
  You are a senior DevOps engineer specializing in Kubernetes, Terraform, and CI/CD.
  Provide production-ready answers with security considerations.
  Use YAML/HCL code blocks where appropriate.
EOF

# Use the role
cat k8s-deployment.yaml | aichat --role devops "Review this deployment manifest"

Using Ollama for Local LLM Inference from Bash

Ollama runs open-weight models entirely on your local machine. No API keys, no cost per token, no data leaving your system — critical for proprietary codebases and sensitive logs.

Installation and Setup

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

# Start the Ollama service
ollama serve &

# Pull models
ollama pull llama3.2
ollama pull mistral
ollama pull codellama
ollama pull phi3

# List downloaded models
ollama list

Running Models from the CLI

# Interactive chat
ollama run llama3.2

# One-shot prompt (pipe-friendly)
echo "Explain kernel panic in 2 sentences" | ollama run llama3.2

# Pipe file contents
cat main.rs | ollama run codellama "Review this Rust code for memory safety issues"

# Use the REST API directly with curl
curl -s http://localhost:11434/api/generate 
  -H "Content-Type: application/json" 
  -d '{
    "model": "llama3.2",
    "prompt": "Summarize the CAP theorem",
    "stream": false
  }' | jq -r '.response'

Ollama REST API in Scripts

# Function wrapping the Ollama API
ollama_ask() {
  local model="${1:-llama3.2}"
  local prompt="$2"
  curl -s http://localhost:11434/api/generate 
    -H "Content-Type: application/json" 
    -d "$(jq -n --arg m "$model" --arg p "$prompt" 
      '{model: $m, prompt: $p, stream: false}')" 
  | jq -r '.response'
}

# Use it
ollama_ask llama3.2 "What is a race condition?"

# Pipe to it
cat error.log | xargs -I{} ollama_ask codellama "Explain this error: {}"

Combining with Shell Tools: fzf, jq, awk, xargs

fzf for Interactive Model Selection

# Interactively pick an llm model
llm_pick() {
  local model
  model=$(llm models | awk '{print $1}' | fzf --prompt="Select model: ")
  llm -m "$model" "$@"
}

# Use it
llm_pick "Explain systemd unit files"

jq for Structured LLM Output

# Force JSON output and parse it
ps aux --sort=-%cpu | head -20 | 
  llm -s "Respond with valid JSON only." 
      "Return top 5 CPU-consuming processes as JSON array: [{pid, user, cpu, command}]" | 
  jq '.[] | select(.cpu > 10)'

xargs for Batch Processing

# Summarize every .log file in a directory
find /var/log -name "*.log" -size +10k | head -10 | 
  xargs -I{} sh -c 'echo "=== {} ===" && tail -50 "{}" | llm "Summarize key events in 2 sentences"'

# Explain each TODO comment in a codebase
grep -rn "TODO:" ./src --include="*.py" | 
  xargs -I{} llm "Explain what work is needed for this TODO: {}"

awk Preprocessing Before LLM

# Extract only ERROR lines before sending to LLM
awk '/ERROR/{print NR": "$0}' application.log | 
  llm "Group these errors by type and suggest the most critical fix"

# Parse Apache access logs for anomalies
awk '{print $1, $7, $9}' /var/log/apache2/access.log | 
  sort | uniq -c | sort -rn | head -50 | 
  llm "Identify suspicious patterns in this web traffic summary"

Writing Bash Functions That Call LLMs

Add these to your ~/.bashrc or ~/.bash_profile for permanent availability.

# ~/.bashrc additions

# Quick AI question from anywhere
ask() {
  llm "$*"
}

# Summarize any file or piped content
summarize() {
  if [ -n "$1" ]; then
    cat "$1" | llm "Provide a concise summary of this content"
  else
    llm "Provide a concise summary of this content"
  fi
}

# Fix last failed command
fix_last() {
  local last_cmd
  last_cmd=$(fc -ln -1 | xargs)
  local error_output
  error_output=$(eval "$last_cmd" 2>&1)
  echo "Command: $last_cmd"
  echo "Error: $error_output"
  echo "$error_output" | llm "This command failed: '$last_cmd'. Explain the error and provide the corrected command."
}

# Generate a .gitignore for a given language/framework
gen_gitignore() {
  llm "Generate a comprehensive .gitignore file for a $* project. 
       Output only the .gitignore content, no explanation." > .gitignore
  echo ".gitignore created for $*"
}

# Review a pull request diff
review_pr() {
  git diff "${1:-main}...HEAD" | 
    llm -s "You are a senior engineer doing a code review. Be constructive and specific." 
    "Review this pull request diff. Identify bugs, security issues, and improvements."
}

The Command Explainer: A Complete Bash Function

This is one of the most practical LLM shell integrations you can build: a function that explains any command you pass or just executed.

# Add to ~/.bashrc

explain() {
  local cmd="$*"
  
  if [ -z "$cmd" ]; then
    # If no argument, explain the last command from history
    cmd=$(fc -ln -1 | xargs)
    echo "Explaining last command: $cmd"
    echo "---"
  fi
  
  llm -s "You are a Linux command-line expert. 
          Break down shell commands into their components. 
          For each part explain: what it does, important flags, and potential gotchas.
          Keep explanations practical and concise." 
      "Explain this shell command in detail: $cmd"
}

# Bonus: explainfile reads a script and explains it section by section
explainfile() {
  if [ -z "$1" ]; then
    echo "Usage: explainfile " return 1 fi cat "$1" | llm -s "You are a Linux shell scripting expert." "Explain what this shell script does, section by section. Note any potential issues or improvements."}
# Usage examples:explain "awk 'NR%2==0' file.txt"explain "ss -tulpn | grep LISTEN"explain "openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem"# Explain the last command you ranexplain# Explain a script fileexplainfile deploy.sh

You can extend this further with a keybinding in your shell:

# Add to ~/.bashrc — bind Ctrl+E to explain the current lineexplain_current_line() { local cmd="${READLINE_LINE}" if [ -n "$cmd" ]; then echo "" explain "$cmd" fi}bind -x '"C-e": explain_current_line'

Cost and Rate Limit Awareness When Scripting

Cloud LLM APIs charge per token and enforce rate limits. A careless loop can cost real money or trigger throttling at the worst moment.

Estimate Token Costs Before Running

# Check how many tokens a file would consume (rough estimate: 1 token ≈ 4 chars)estimate_tokens() { local chars=$(wc -c < "$1") echo "Estimated tokens: $((chars / 4))" echo "Approx GPT-4o-mini cost: $$(echo "scale=6; $chars / 4 / 1000000 * 0.15" | bc)"}estimate_tokens large_logfile.txt

Rate Limiting in Loops

# Process files with a sleep to avoid rate limit errorsfor file in ./logs/*.log; do echo "Processing $file..." tail -100 "$file" | llm "Summarize errors in this log" >> summaries.txt sleep 2 # Stay under rate limitsdone# Use xargs with concurrency controlfind . -name "*.py" | xargs -P 2 -I{} sh -c 'cat "{}" | llm "Describe what this Python module does in one sentence" >> module_docs.txt && sleep 1'

Caching LLM Results

# Cache LLM responses to avoid redundant API callscached_llm() { local cache_dir="$HOME/.cache/llm_cache" mkdir -p "$cache_dir" local input="$1" local prompt="$2" local cache_key cache_key=$(echo "${input}${prompt}" | md5sum | cut -d' ' -f1) local cache_file="$cache_dir/$cache_key" if [ -f "$cache_file" ]; then cat "$cache_file" else echo "$input" | llm "$prompt" | tee "$cache_file" fi}# Use it: identical inputs hit the cachecached_llm "$(cat config.yaml)" "Find security issues"

Monitoring Usage

# llm logs track all your queries — monitor themllm logs list --count 20# Count tokens used today (rough)llm logs list --since today --json | jq '[.[].prompt | length] | add' | awk '{print "Characters sent today: "$1, "| Est tokens: "$1/4}'# Set a daily reminder if you're scripting heavily# Add to a cron job: crontab -e# 0 9 * * * llm logs list --since yesterday --json | jq 'length' | # xargs -I{} notify-send "LLM queries yesterday: {}"

Using Local Models for Sensitive or High-Volume Work

# Route sensitive data to Ollama, public queries to cloudsmart_llm() { local sensitivity="$1" # "local" or "cloud" local prompt="$2" if [ "$sensitivity" = "local" ]; then echo "$prompt" | ollama run llama3.2 else llm "$prompt" fi}# Process internal code locally, public docs via cloudcat internal_api.py | smart_llm local "Find bugs in this code"curl -s https://docs.python.org/3/ | smart_llm cloud "Summarize the Python 3 docs homepage"

The combination of llm, aichat, and Ollama gives you a full spectrum of inference options: fast cloud models for interactive use, local models for sensitive data and high-volume batch work, and a set of composable shell primitives that integrate naturally into existing workflows. The functions and patterns here are starting points — the real power comes from adapting them to your specific environment, piping the exact data sources you work with daily, and building a personal library of battle-tested LLM helpers in your .bashrc.


Go up