DEV Community

Dakota Huang
Dakota Huang

Posted on

Exit Code Zero Is Not a Clean Audit: Diff the Process Tree Before You Trust a Free Model's Shell

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
Enter fullscreen mode Exit fullscreen mode

What the diff tells you

  • socks_<after>.txt gains 127.0.0.1:8765.
  • proc_<after>.txt gains a Python child whose parent has exited.
  • fds_<after>.txt shows 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>/fd may 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 ss is missing from a minimal image, install iproute2 or capture /proc/net/tcp directly.
  • 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,
  • auditd or 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

  1. Start a fresh free server instance.
  2. Generate the command with free model access.
  3. Snapshot process, socket, and fd state.
  4. Run the command.
  5. Snapshot again.
  6. Diff, then kill any orphan process you did not expect.
  7. 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)