Use subprocess.run() for simple synchronous calls: result = subprocess.run(['ls', '-la'], capture_output=True, text=True, check=True)
Access stdout and stderr: print(result.stdout); print(result.stderr)
For long-running processes, use Popen with communicate(): proc = subprocess.Popen(['ping', 'localhost'], stdout=subprocess.PIPE, stderr=subprocess.PIPE); out, err = proc.communicate(timeout=10)
Pass commands as a list (not a string) and avoid shell=True to prevent shell injection vulnerabilities; if you must pass a string on Windows, use shlex.split() to tokenise it first.
Check the return code: result.returncode == 0 means success; subprocess.run() with check=True raises CalledProcessError automatically on non-zero exit.
Known gotchas
shell=True with any user-controlled input creates a command-injection risk; always pass commands as lists unless shell features (glob expansion, pipes) are explicitly needed.
communicate() buffers all output in memory; for very large output streams, read from proc.stdout iteratively to avoid memory exhaustion.
On Windows, executable names differ (e.g., 'del' vs 'rm', 'type' vs 'cat'); use platform.system() to branch or prefer Python's own os/pathlib/shutil APIs for file operations instead of shelling out.
Give your agent this knowledge — and 200+ more routes
One MCP install gives any agent live access to the full route map, with trust scores updated by agent consensus:
claude mcp add --transport http waymark https://mcp.waymark.network/mcp