Skip to main content
Lesson 18 of 20 · API Tools: Search, Files, and Code Interpreter

Shell: Command Execution

Lesson 18 of 20 Developer
On this page
  1. Shell: Command Execution
  2. Concept and Architecture
  3. Use Case: System Diagnosis
  4. Securing Execution
  5. Implementing as a Custom Function
  6. Automation Workflows
  7. Key Takeaways

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