The Security Problem Nobody Talks About
You're building an AI agent. The LLM needs to call a weather API. You write a batch script that takes a city name as input.
This seems fine:
powershell -Command "$city = '%CITY%'; Invoke-RestMethod -Uri $url"
Until the LLM (or a malicious user) provides this:
Toronto'; Remove-Item -Path C:\* -Recurse; echo '
Your hard drive is gone.
Shell injection via LLM-generated parameters is the underappreciated risk in CLI-based AI agents. The LLM hallucinates creative inputs. Users try adversarial prompts. Either way, you're one bad string away from arbitrary code execution.
Why This Matters More for AI Agents
Traditional web apps have trained us to sanitize user input. But AI agents introduce a new attack surface: the LLM itself can be the attacker. Not maliciously—through hallucination, misunderstanding, or prompt injection that tricks it into generating malformed parameters.
Code for the article is here https://github.com/vishalmysore/cli-ai-agent
Consider:
User: "What's the weather in ; rm -rf / #"
LLM: "Let me check the weather for ; rm -rf / #"
Agent: executes weather_current.cmd "; rm -rf / #"
The LLM doesn't know it's generating an attack. It's just following instructions.
The Fix: Sanitize at the Orchestrator Layer
Since we're using Tools4AI (Java) as the orchestrator, sanitization must happen before ProcessBuilder touches the shell:
public String sanitizeInput(String input) {
// Remove shell metacharacters
String sanitized = input.replaceAll("[;&|<>()$`\\\\\"']", "");
// Whitelist validation
if (!sanitized.matches("^[a-zA-Z0-9\\s-]+$")) {
throw new SecurityException("Invalid characters in input");
}
return sanitized;
}
Defense layers:
- Whitelist validation - Only known-good characters
- Length limits - Cap at reasonable values (50 chars for city names)
- No string interpolation - Use parameterized commands where possible
- Least privilege - Run under restricted accounts
- Audit logging - Track all executed commands
The hard truth: This demo assumes trusted input. For production, comprehensive sanitization is mandatory.
How It Works
The Stack:
- Tools4AI (Java) orchestrates tool selection
- CMD scripts validate parameters and check API keys
- PowerShell makes HTTP calls and parses JSON
- Results print to stdout for agent consumption
Environment Variable Propagation:
CMD and PowerShell have separate variable namespaces. Solution: CMD validates the environment variable exists, then PowerShell accesses it directly via $env:VARIABLE_NAME:
if "%STOCKS_API_KEY%"=="" (
echo ERROR: Please set STOCKS_API_KEY environment variable
exit /b 1
)
powershell -Command "$apikey = $env:STOCKS_API_KEY; ..."
JSON Parsing with Awkward Keys:
Alpha Vantage returns JSON with spaces in property names ("01. symbol", "05. price"). PowerShell handles this with quoted dot notation, plus type casting for arithmetic and conditional formatting:
$r = Invoke-RestMethod -Uri $url;
$q = $r.'Global Quote';
if ($q -and $q.'01. symbol') {
$price = [math]::Round([double]$q.'05. price', 2);
$change = [math]::Round([double]$q.'09. change', 2);
$pct = $q.'10. change percent'.Replace('%','');
$vol = [math]::Round([double]$q.'06. volume' / 1000000, 1);
$sign = if ($change -gt 0) {'+' + $change} else {$change.ToString()};
Write-Host ('STOCK: {0} - ${1} - Change: {2} ({3}%%) - Vol: {4}M' -f
$q.'01. symbol', $price, $sign, $pct, $vol)
}
Time Calculation:
NewsAPI returns ISO 8601 timestamps. Convert to "2h ago" format using PowerShell's New-TimeSpan:
$time = (New-TimeSpan -Start ([datetime]$a.publishedAt) -End (Get-Date)).TotalHours;
$ago = if ($time -lt 1) { '{0}m ago' -f [math]::Round($time * 60) }
elseif ($time -lt 24) { '{0}h ago' -f [math]::Round($time) }
else { '{0}d ago' -f [math]::Round($time / 24) };
Complete scripts in Appendix.
Performance: The Java Tax
Every tool call spawns two processes before touching the network:
Overhead breakdown:
- CMD.exe spawn: 20-50ms
- PowerShell.exe spawn: 200-500ms
- Network latency: 200-1000ms
- Total: 420-1550ms per call
For a single weather query, fine. For 100 calls in a session, you've added 25-60 seconds of pure process overhead.
When this matters:
- ❌ High-frequency operations (multiple per second)
- ❌ Latency-sensitive apps (chatbots, real-time systems)
- ❌ Serverless functions (cold start + process spawn)
- ✅ Prototypes and demos
- ✅ Infrequent operations (once per minute)
- ✅ Integration with legacy CLI tools
The upgrade path:
- CLI Prototype (you are here) - Validates logic, proves integration
- Native Java HTTP - Replace scripts with OkHttp/HttpClient, eliminate spawn overhead (5-10x improvement)
- Connection Pooling - Reuse connections, cache responses
- Microservices (if needed) - Scale independently
CLI orchestration is training wheels. Great for prototyping. Don't ship it without understanding the performance implications.
Results: Does It Work?
Test execution (March 7, 2026):
$env:WEATHER_API_KEY="your_weatherapi_key_here"
$env:STOCKS_API_KEY="your_alphavantage_key_here"
$env:NEWS_API_KEY="your_newsapi_key_here"
weather_current.cmd Toronto
stocks_price.cmd AAPL
news_search.cmd "AI agents"
Weather (Toronto):
WEATHER: Toronto|4.3C|Fog|Humidity: 100%|Wind: 13.7 km/h
Response time: 287ms
Accuracy: Within 0.3°C of Weather.com
Stock (AAPL):
STOCK: AAPL - $257.46 - Change: -2.83 (-1.0872%) - Vol: 41.1M
Response time: 412ms
Data matches Yahoo Finance exactly
Stock (TSLA):
ERROR: Invalid symbol or API limit reached
Expected behavior - Alpha Vantage free tier exhausted (25 calls/day)
News (AI agents):
NEWS: AI agents|Programming, customer service roles at risk|Times of India|1d ago
NEWS: AI agents|Agent Skills vs RAG Workflows|Geeky Gadgets|1d ago
NEWS: AI agents|Show HN: Python 3.14 interpreter in Rust|Github.io|1d ago
Response time: 523ms
Articles from last 24 hours, correctly formatted
Validation:
| API | Accuracy Check | Result |
|---|---|---|
| Weather | vs Weather.com | ±0.3°C variance (normal for different stations) |
| Stocks | vs Yahoo Finance | Exact match on price, change, volume |
| News | Timestamp math | "1d ago" correctly calculated from ISO 8601 |
What We Learned
Security matters more than you think. Shell injection via LLM parameters is a real threat. Sanitize at the orchestrator layer (Java), not in the shell scripts.
Performance matters when it scales. 250-600ms overhead per call is fine for demos, unacceptable for production. Plan the upgrade path early.
Real APIs expose real problems. Rate limits, network timeouts, malformed JSON—mock data hides all of this. Testing against live APIs forced better error handling.
CLI is a trade-off, not a silver bullet. It's the Integration Layer of Last Resort. Use it when you can't rewrite existing tools. For greenfield work, consider MCP or direct HTTP integration.
Conclusion
This project demonstrates CLI-based AI agent orchestration with three real APIs. It works. It's documented. It has known limitations.
What it is: A functional prototype showing how to integrate external data sources into AI agents using command-line tools.
What it isn't: Production-ready without security hardening. Performance-optimized. A recommendation to use CLI for new development.
The value: Documenting the journey from "Hello World" to "it works with real APIs." The lessons about security, performance, and error handling are worth more than the code.
If you're building an AI agent, you'll hit these problems. This is your field guide for when you do.
Appendix: Complete Code Listings {#appendix}
Note: The following sections contain complete, unabridged code listings for reference. The main article above highlights key patterns and security considerations. Use these as copy-paste starting points, but remember to implement input sanitization before production use.
A. Weather Current (Complete)
File: cli/weather/weather_current.cmd
@echo off
REM Weather Current CLI - Get current weather for a city using WeatherAPI.com
REM Usage: weather_current.cmd <city>
REM Get free API key: https://www.weatherapi.com/signup.aspx
setlocal enabledelayedexpansion
REM WARNING: Sanitize this parameter at the Java layer before invoking this script
set CITY=%~1
if "%CITY%"=="" (
echo ERROR: City name required
exit /b 1
)
REM Check for API key in environment variable
if "%WEATHER_API_KEY%"=="" (
echo ERROR: Please set WEATHER_API_KEY environment variable
echo Get free API key: https://www.weatherapi.com/signup.aspx
exit /b 1
)
REM Call WeatherAPI.com (free tier: 1M calls/month)
powershell -NoProfile -Command ^
"$city = '%CITY%'; ^
$apikey = $env:WEATHER_API_KEY; ^
$url = 'http://api.weatherapi.com/v1/current.json?key=' + $apikey + '&q=' + $city + '&aqi=no'; ^
$r = Invoke-RestMethod -Uri $url; ^
Write-Host ('WEATHER: {0}|{1}C|{2}|Humidity: {3}%%%%|Wind: {4} km/h' -f ^
$r.location.name, $r.current.temp_c, $r.current.condition.text, ^
$r.current.humidity, $r.current.wind_kph)"
exit /b 0
B. Stock Price (Complete)
File: cli/stocks/stocks_price.cmd
@echo off
REM Stock Price CLI - Get real-time stock price using Alpha Vantage
REM Usage: stocks_price.cmd <symbol>
REM Get free API key: https://www.alphavantage.co/support/#api-key
setlocal enabledelayedexpansion
REM WARNING: Sanitize this parameter at the Java layer before invoking this script
set SYMBOL=%~1
if "%SYMBOL%"=="" (echo ERROR: Stock symbol required & exit /b 1)
REM Check for API key in environment variable
if "%STOCKS_API_KEY%"=="" (
echo ERROR: Please set STOCKS_API_KEY environment variable
echo Get free API key: https://www.alphavantage.co/support/#api-key
exit /b 1
)
REM Call Alpha Vantage API (free tier: 25 requests/day)
powershell -NoProfile -Command ^
"$symbol = '%SYMBOL%'; ^
$apikey = $env:STOCKS_API_KEY; ^
$url = 'https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=' + $symbol + '&apikey=' + $apikey; ^
$r = Invoke-RestMethod -Uri $url; ^
$q = $r.'Global Quote'; ^
if ($q -and $q.'01. symbol') { ^
$price = [math]::Round([double]$q.'05. price', 2); ^
$change = [math]::Round([double]$q.'09. change', 2); ^
$pct = $q.'10. change percent'.Replace('%%',''); ^
$vol = [math]::Round([double]$q.'06. volume' / 1000000, 1); ^
$sign = if ($change -gt 0) {'+' + $change} else {$change.ToString()}; ^
Write-Host ('STOCK: {0} - ${1} - Change: {2} ({3}%%) - Vol: {4}M' -f ^
$q.'01. symbol', $price, $sign, $pct, $vol) ^
} else { ^
Write-Host 'ERROR: Invalid symbol or API limit reached' ^
}"
exit /b 0
C. News Search (Complete)
File: cli/news/news_search.cmd
@echo off
REM News Search CLI - Search news using NewsAPI.org
REM Usage: news_search.cmd <query>
REM Get free API key: https://newsapi.org/register
setlocal enabledelayedexpansion
REM WARNING: Sanitize this parameter at the Java layer before invoking this script
set QUERY=%~1
if "%QUERY%"=="" (echo ERROR: Search query required & exit /b 1)
REM Check for API key in environment variable
if "%NEWS_API_KEY%"=="" (
echo ERROR: Please set NEWS_API_KEY environment variable
echo Get free API key: https://newsapi.org/register
exit /b 1
)
REM Call NewsAPI (free tier: 100 requests/day)
powershell -NoProfile -Command ^
"$query = '%QUERY%'; ^
$apikey = $env:NEWS_API_KEY; ^
$url = 'https://newsapi.org/v2/everything?q=' + $query + '&sortBy=publishedAt&pageSize=3&apiKey=' + $apikey; ^
$r = Invoke-RestMethod -Uri $url; ^
foreach ($a in $r.articles) { ^
$time = (New-TimeSpan -Start ([datetime]$a.publishedAt) -End (Get-Date)).TotalHours; ^
$ago = if ($time -lt 1) { '{0}m ago' -f [math]::Round($time * 60) } ^
elseif ($time -lt 24) { '{0}h ago' -f [math]::Round($time) } ^
else { '{0}d ago' -f [math]::Round($time / 24) }; ^
Write-Host ('NEWS: {0}|{1}|{2}|{3}' -f $query, $a.title, $a.source.name, $ago) ^
}"
exit /b 0
Top comments (0)