Shell: Command Execution
On this page
Shell: Command Execution
This is the most powerful tool in this course, and the only one where a mistake can turn its power against you. Other tools read, calculate, or search; this one acts on a machine. A poorly phrased system diagnosis remains just a diagnosis; a poorly phrased cleanup command deletes files.
This lesson covers usage and safeguards together because they cannot be separated. The golden rule: the model must never execute a command you haven’t explicitly authorized in advance.
Concept and Architecture
The difference from Code Interpreter is critical, and it comes down to one word: isolation. Code Interpreter runs in a disposable, network-isolated sandbox where the worst-case scenario is a wrong calculation. Shell runs in an environment you configure—potentially your local machine, potentially a server—where the worst-case scenario is an irreversible operation.
This freedom is the point: without it, automated deployment and maintenance are impossible. It does, however, require you to consciously choose the execution scope. A dedicated container with limited privileges is almost always the right choice over direct host access.
from openai import OpenAI
client = OpenAI()
# The Shell tool is used via the Agents SDK
# because it requires a controlled execution environment
from agents import Agent, Runner
agent = Agent(
name="devops-assistant",
model="gpt-5.6-terra",
instructions=(
"You are a DevOps assistant. You execute system commands "
"to diagnose and resolve infrastructure issues. "
"Explain each command before executing it."
),
tools=[{"type": "shell"}]
)
Use Case: System Diagnosis
# The agent can execute diagnostic commands
agent = Agent(
name="diagnostic",
model="gpt-5.6-terra",
instructions=(
"Diagnose the system state by executing appropriate commands. "
"Start with basic checks (disk, memory, CPU) "
"then dig deeper based on symptoms."
),
tools=[{"type": "shell"}]
)
# The agent will execute commands like:
# df -h -> disk space
# free -m -> memory
# top -bn1 -> CPU load
# systemctl status nginx -> service status
Securing Execution
Three safeguards work together, and none is sufficient on its own. The allowlist is the foundation: you enumerate what is permitted rather than what is forbidden, because a blocklist always misses a variant—and it only takes one. A timeout prevents a stuck command from hanging the system. A restricted working directory limits the damage if a legitimate command points to the wrong location.
A fourth safeguard, non-technical but worth the other three combined: require human confirmation for any destructive or irreversible operation. A model makes mistakes with the same confidence as when it is right.
# Pattern: wrapper function with validation
import subprocess
import shlex
ALLOWED_COMMANDS = {
"diagnostic": ["df", "free", "top", "uptime", "ps", "netstat", "curl", "ping"],
"deployment": ["git", "docker", "npm", "pip"],
"files": ["ls", "cat", "head", "tail", "wc", "grep", "find"],
}
FORBIDDEN_COMMANDS = ["rm", "dd", "mkfs", "shutdown", "reboot", "kill", "passwd"]
def execute_secure_command(command: str, category: str = "diagnostic") -> dict:
"""Executes a command with security validation."""
# Parse the command
parts = shlex.split(command)
executable = parts[0]
# Check prohibitions
if executable in FORBIDDEN_COMMANDS:
return {
"error": f"Forbidden command: {executable}",
"reason": "This command is on the blocklist"
}
# Check permissions
allowed = ALLOWED_COMMANDS.get(category, [])
if executable not in allowed:
return {
"error": f"Command not allowed in category '{category}'",
"allowed": allowed
}
# Execute with timeout
try:
result = subprocess.run(
parts,
capture_output=True,
text=True,
timeout=30,
cwd="/tmp" # Restricted working directory
)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"return_code": result.returncode
}
except subprocess.TimeoutExpired:
return {"error": "Timeout: command exceeded 30 seconds"}
Implementing as a Custom Function
Implementing this yourself via function calling offers an advantage you shouldn’t view as a fallback: validation lives in your code. You decide the allowlist, the logging, and the confirmation steps, and you can audit everything. For a tool with this level of risk, keeping control of the checkpoint is preferable, even when a native version becomes available.
tools = [
{
"type": "function",
"name": "execute_command",
"description": (
"Execute a shell command on the server. "
"Allowed commands: system diagnostics (df, free, top, uptime, ps), "
"file reading (ls, cat, head, tail, grep), "
"network (curl, ping, netstat). "
"Destructive commands are forbidden."
),
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
},
"directory": {
"type": "string",
"description": "Working directory (default: /tmp)"
}
},
"required": ["command"],
"additionalProperties": False
},
"strict": True
}
]
import json
def handle_calls(response):
results = []
for item in response.output:
if item.type == "function_call" and item.name == "execute_command":
args = json.loads(item.arguments)
result = execute_secure_command(
args["command"],
category="diagnostic"
)
results.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result)
})
return results
# Full loop
response = client.responses.create(
model="gpt-5.6-terra",
input="Check disk space and available memory",
tools=tools
)
results = handle_calls(response)
final_response = client.responses.create(
model="gpt-5.6-terra",
input=response.output + results,
tools=tools
)
print(final_response.output_text)
Automation Workflows
Automated Deployment
Deployment is the most valuable use case, provided you understand what you are delegating. The model does not invent the procedure: it chains together steps you have defined, verifies intermediate outputs, and stops on any anomaly. It is an attentive operator, not a decision-maker.
tools_deploy = [
{
"type": "function",
"name": "execute_deploy_step",
"description": (
"Execute a deployment step. "
"Possible steps: git_pull, tests, build, deploy, rollback. "
"Each step is validated before proceeding to the next."
),
"parameters": {
"type": "object",
"properties": {
"step": {
"type": "string",
"enum": ["git_pull", "tests", "build", "deploy", "rollback"]
},
"parameters": {
"type": "object",
"properties": {
"branch": {"type": "string"},
"environment": {"type": "string", "enum": ["staging", "production"]}
}
}
},
"required": ["step"],
"additionalProperties": False
},
"strict": True
}
]
def execute_step(step: str, params: dict) -> dict:
scripts = {
"git_pull": "cd /app && git pull origin {branch}",
"tests": "cd /app && npm test",
"build": "cd /app && npm run build",
"deploy": "cd /app && ./deploy.sh {environment}",
"rollback": "cd /app && ./rollback.sh {environment}"
}
command = scripts[step].format(**params)
return execute_secure_command(command, "deployment")
Monitoring and Alerts
Monitoring reverses the logic of the rest of this lesson: the commands here are read-only, so they are harmless, and the value lies in the interpretation. A model that reads disk space, load, and error logs spots correlations that an isolated threshold misses—making this the safest entry point for starting with this tool.
def check_system_health() -> dict:
"""Checks system health and returns a report."""
checks = {}
# Disk space
r = execute_secure_command("df -h /", "diagnostic")
checks["disk"] = r
# Memory
r = execute_secure_command("free -m", "diagnostic")
checks["memory"] = r
# Services
for service in ["nginx", "postgresql", "redis"]:
r = execute_secure_command(
f"systemctl is-active {service}",
"diagnostic"
)
checks[f"service_{service}"] = r
return checks
# The AI analyzes the results and suggests actions
response = client.responses.create(
model="gpt-5.6-terra",
input=f"Analyze this system health report and identify issues:\n"
f"{json.dumps(check_system_health(), indent=2)}",
tools=tools
)
Key Takeaways
- The Shell tool connects the model to your infrastructure
- Implement allowlists and blocklists for commands
- Use timeouts and restricted working directories
- Function calling lets you implement Shell as a custom tool
- Combine Shell with other tools for complete DevOps workflows