Running arbitrary shell commands or scripts as agents: sandboxing considerations, stdout/stderr capture, exit-code semantics, environment variable injection, and security best practices.
Build agents that run shell scripts or Python programs instead of calling an LLM, useful for data pipelines and deterministic processing steps.
Not every agent needs a language model. Use the shell adapter when:
- The task is fully deterministic (format conversion, data extraction, validation)
- You already have a working Python/Node script
- LLM costs need to be zero for high-frequency tasks
- You need to access local files or databases the LLM can't reach
{
"adapter_type": "shell",
"adapter_config": {
"command": "python3 /home/agents/etl.py",
"timeout_ms": 30000,
"working_dir": "/home/agents"
}
}
The assembled task prompt is passed as the first command-line argument AND piped to stdin:
# Equivalent to what the adapter does:
echo "${TASK_PROMPT}" | python3 /home/agents/etl.py "${TASK_PROMPT}"
Your script reads from stdin (or sys.argv[1]) and writes the result to stdout. Exit code 0 = success, non-zero = failure.
#!/usr/bin/env python3
# /home/agents/csv_summariser.py
import sys
import json
import csv
import io
task = sys.stdin.read()
# Extract CSV path from task description (simple heuristic)
lines = task.split('\n')
csv_path = next((l.split(': ')[1] for l in lines if l.startswith('FILE:')), None)
if not csv_path:
print("ERROR: No FILE: line found in task", file=sys.stderr)
sys.exit(1)
with open(csv_path) as f:
reader = csv.DictReader(f)
rows = list(reader)
print(f"## CSV Summary\n")
print(f"- **Rows**: {len(rows)}")
print(f"- **Columns**: {', '.join(rows[0].keys()) if rows else 'empty'}")
print(f"- **First row**: {json.dumps(rows[0]) if rows else 'N/A'}")
Issue description format for this agent:
FILE: /data/sales_q1.csv
Summarise the structure and first row of this CSV file.
{
"adapter_config": {
"command": "node /home/agents/loader.js",
"env": {
"DB_URL": "{{env.DATABASE_URL}}",
"API_KEY": "{{env.STRIPE_KEY}}"
}
}
}
Shell agents incur no token cost. A cost_event is still recorded with cost_usd = 0 and model = 'shell', so the audit trail stays complete.