Exit code zero does not prove the shell command did nothing else.
A model may return a command that exits 0 after starting a listener, a child daemon, or a cron entry. If you only check $?, you miss everything that stays behind.
Exit codes report how the process terminated. They do not report what the process did before it terminated, what it left running, or what it bound while you were not looking.
What actually matters after a command
- Process tree: new PIDs, parent-child relationships, lifetimes.
- Listening sockets: ports the process opened and bound.
- Open file descriptors: logs, pipes, deleted binaries.
- File changes: this article deliberately ignores filesystem diffing; process and socket state is the missing half.
Why this fits a free model + free server setup
MonkeyCode's free tier includes model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
When a model generates commands, the free server should be the first place they run, not your laptop. Disposable infrastructure lowers the cost of making mistakes. You can let a model try a command, observe side effects, and destroy the box. That changes the failure mode from risky to recoverable.
The snapshot-diff workflow below runs on any typical Linux server and uses only common utilities.
The artifact: snapshot before and after
Run this on a fresh server. It records three views: processes, listening sockets, and file descriptors.
#!/usr/bin/env bash
set -u
snapshot() {
stamp=$(date +%s)
ps -eo pid,ppid,user,etime,args > proc_${stamp}.txt
ss -tulpn > socks_${stamp}.txt
find /proc/[0-9]*/fd -maxdepth 1 -type l -printf '%p -> %l\n' 2>/dev/null \
| sort > fds_${stamp}.txt
echo $stamp
}
before=$(snapshot)
# Replace this with the model-generated command.
python3 - <<'PY' > /dev/null 2>&1 &
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'ok')
HTTPServer(('127.0.0.1', 8765), H).serve_forever()
PY
# Give the background server a moment to bind.
sleep 1
after=$(snapshot)
echo 'Process diff:'
diff <(sort proc_${before}.txt) <(sort proc_${after}.txt) || true
echo 'Socket diff:'
diff <(sort socks_${before}.txt) <(sort socks_${after}.txt) || true
echo 'FD diff:'
diff <(sort fds_${before}.txt) <(sort fds_${after}.txt) || true
What the diff tells you
-
socks_<after>.txtgains127.0.0.1:8765. -
proc_<after>.txtgains a Python child whose parent has exited. -
fds_<after>.txtshows the socket fd and cwd.
The original command still exits 0.
Verify with a negative test first
Do not trust a blank run. Start with a no-op such as true, snapshot before and after, and confirm the diff is only system noise.
Then run nohup python3 -m http.server 9000 & and verify your snapshot catches it. If it does not, your baseline is masking child processes, not that the command is safe.
Keep a small suite of known-bad examples: a background listener, a background process that writes to /tmp, and a command that tries to reach an internal address. Run them and confirm the diff flags each one.
Limits you should write down
- A process can start and exit between snapshots. You can miss short-lived calls.
-
/proc/<pid>/fdmay need root or same-user access. - A server that calls an external API is not visible in local sockets alone.
- System services can create false positives. Snapshot on a fresh server and discard known PIDs.
- If
ssis missing from a minimal image, installiproute2or capture/proc/net/tcpdirectly. - This is an observation layer, not a runtime policy. It will not block malicious commands.
When to rely on heavier controls
Use this workflow for cheap first-pass triage on disposable boxes, not regulated workloads.
If the command can change accounts, write files, or reach production, add:
- a non-root user,
- a seccomp or Landlock policy,
-
auditdor eBPF, - an allowlist proxy for network egress.
The snapshot only helps you notice after the fact. Runtime controls stop the action before it happens.
After you find an orphan, do not just kill it. Ask whether the command should have had permission to spawn anything in the first place. If the answer is no, the boundary is too wide.
A shorter checklist
- Start a fresh free server instance.
- Generate the command with free model access.
- Snapshot process, socket, and fd state.
- Run the command.
- Snapshot again.
- Diff, then kill any orphan process you did not expect.
- Record the result before you reuse the server.
Who should skip this
If you only run trusted commands that you wrote yourself, the workflow is overhead. If you need a real security boundary, use OS isolation instead of a diff script.
On your next free server run, start with the negative test, not the prompt.
Top comments (0)