Study material
concept 1 · operating system

The POSIX contract and exit 2

The number 2 Nemesis returns when it blocks did not come out of nowhere: it is a direct product of the POSIX process model. That number was chosen for a reason: it became a contract IDEs understand without ambiguity.

In 30 seconds

Every Unix process ends by returning an integer to its parent: the exit code. Zero is success, non-zero is failure. Agent IDEs agreed that, in the hook that runs before the action, exiting with exit 2 means "block this action". The hook reads the agent request from standard input (stdin), decides, and when it is hostile calls exit(2). Deterministic: not a request, but the end of a process with a verdict.

The agent requests an action; the pretool hook reads it via stdin and decides; the process exits with 0 (IDE allows) or 2 (IDE cancels). The number comes from the kernel.
The agent asks, the hook decides, the process ends with 0 (allow) or 2 (cancel). The number comes from the kernel.

From scratch: where the exit code comes from

In the POSIX model, a process is created by fork/exec and, when it dies, hands the parent a status via wait. From that status the parent reads one byte: the exit code. The convention is old and universal: 0 means "it worked", any value from 1 to 255 means "it failed", and each program can assign meanings to the non-zero codes. The agent talks to the hook through three standard descriptors: input (stdin), output (stdout) and error (stderr). The hook receives the request on stdin as JSON, processes it, and what matters for the decision is not what it prints, but the number it exits with.

The IDE (parent) spawns the hook via fork/exec; the request enters via stdin as JSON; stdout/stderr are diagnostics; the verdict returns to the parent as the exit code via wait.
The process model beneath the contract: fork/exec, stdin carrying the request, and the verdict returning to the parent via wait().

Why it matters

The enforcement needed a signal the agent could not "talk" into changing. Prompt text is probabilistic: the model can ignore it. The exit code of a process is a fact of the operating system. When the IDE agrees on "if the pre-action hook exits with 2, the action is cancelled", control stops depending on the model's goodwill and starts depending on the kernel delivering the process status, which it always does.

How it correlates

This contract ties everything that follows. The pretool is the process that reads stdin and decides; the fail-closed design guarantees that even an internal error ends in exit 2 instead of letting things pass; and the kernel layer uses the kernel-level analog, returning -EPERM instead of 0, to deny execs. The exit code is the userspace form of the same principle: communicating "denied" through a channel that cannot be argued with.

How Nemesis applies it

The blocking point is a single function that logs the reason to the ledger and ends the process with 2. It only records telemetry when the reason is security-related (the vocabulary of NEMESIS messages), because an operational hook error is not protection.

📄 .nemesis/hooks/nemesis-pretool-check-unix.rs:190-194 · o bloqueio que sai com exit 2

if reason.contains("NEMESIS ") {
    nemesis_defender::violations_log::append("pretool", reason);
}
std::process::exit(2);

And where Nemesis creates a child process, the POSIX API is used with the three descriptors explicitly disabled, exactly the fork/exec model plus stdio redirection:

📄 .nemesis/nemesis-defender/src/main.rs:106-126 · spawn do daemon com stdio nulo

std::process::Command::new(&exe)
    .arg("--daemon")
    .current_dir(&project_root)
    .stdin(std::process::Stdio::null())
    .stdout(std::process::Stdio::null())
    .stderr(std::process::Stdio::null())
    .spawn()

Decisions and limits

"2 = blocked" is not POSIX law, it is an agent-IDE convention: POSIX only guarantees 0 = success and ≠0 = failure. The contract depends on the IDE honoring 2 as cancellation, which is exactly why that behavior is validated in the pentest, with the hook actually connected. Each channel's role is also kept separate: what the hook prints to stdout/stderr is diagnostics for the reader; the decision is the exit code alone.

A hung process returns no code at all. The hook is synchronous and fast, with a fail-closed design, precisely so it never hangs without a verdict.

Self-check

Why is the exit code more reliable than a prompt instruction?

Because it is an operating-system fact, delivered by the kernel to the parent process, not a suggestion the model can ignore. The agent has no way to "argue" against a process ending with status 2.

Where does the hook receive the agent request?

Through standard input (stdin), as JSON. It reads, decides, and communicates the verdict through the exit code, not the printed text.

Is "2" required by POSIX?

No. POSIX only fixes 0 = success and ≠0 = failure. The 2 as "blocked" is the convention agreed with agent IDEs; that is why it is treated as a contract and validated in practice.

Further reading

Citações verificadas contra o repositório. Voltar ao índice.