GUIDE · CONCEPT 8 min ·

Process-tree scoped credentialsWhy 'this command' beats 'this session'.

A shell-session credential lives as long as you are logged in. A process-tree credential lives as long as one command runs. Those two sentences describe a very different attack surface.

TL;DR· the answer, in twenty seconds

What: Shell-session credentials are visible to every child process the agent spawns, including tools the agent calls by mistake or in the wrong context. The agent does not need to do anything wrong; Unix inheritance does the work.

Fix: Scope credentials to the specific command that needs them. On Linux use prctl(PR_SET_CHILD_SUBREAPER) to own a subtree; on macOS use kqueue with EVFILT_PROC. When the root process exits, revoke. The audit log entry records exactly what got the credential, when, and for how long.

Lesson: Credential scope is a property of a grant, not a property of a secret. The same STRIPE_KEY can be session-scoped or command-scoped depending on how you deliver it. Delivery mechanism is the lever.

The Replit incident from mid-2024 is the cleanest example. An AI coding agent held a database credential for the entire shell session. The agent was asked to help with a migration script. It ran a DROP command against the wrong database. The credential was valid. The authorization was never checked at the scope level. The agent did not hack anything; it used what it had.

That incident was about blast radius. The agent could do more than it needed to because the credential it held was good for everything, not good for one task.

Most post-mortems focus on the wrong question: "why did the agent do that?" The better question is "why was the credential there at all?" Shell-session scope is the answer. The fix is not better agent instructions. The fix is narrower grants.

Short version: what to know in three minutes

  • A shell-session credential (export DB_URL=...) lives from login until logout. Every child process the shell spawns inherits it. That includes your agent, every tool the agent calls, every subprocess those tools call.
  • A process-tree credential lives from exec to exit of one root process. When that process exits, the grant ends. Children can use it while the process runs. Nothing else can.
  • Unix process inheritance is automatic. You do not need to do anything wrong for the agent to pass a credential down to a tool. You just need to have the variable in the environment.
  • "Why did the grant end?" is a real design question. Process exit, TTL expiry, and explicit revoke are three different answers. Pick one deliberately, or you get unpredictable behavior from credential scoping systems.
  • Double-forking daemons escape process-tree scope. That is a real limit. Document it.

What process-tree scope actually means

When a Unix process starts, it inherits a copy of its parent's environment. fork() duplicates the parent's state. execve() replaces the program image but keeps the environment unless the new program specifically drops it.

That chain is how STRIPE_KEY gets from your .zshrc to a subprocess five levels deep. Nothing passes the value explicitly. Every link in the chain just inherits from its parent.

Process-tree scoping breaks that chain at a specific root. The idea: deliver the credential only into the environment of a specific command, not into the ambient shell. When that command exits, nothing downstream still holds the value.

The implementation has two pieces:

First, deliver the credential only at exec time. Do not export the variable into your shell. Start the command with the variable set only for that invocation:

STRIPE_KEY=$(get-from-vault) stripe trigger payment_intent.created

The STRIPE_KEY exists in the environment of stripe trigger and its children. It is not in your shell's environment. After stripe trigger exits, the value is gone from the process tree.

Second, watch the process tree and revoke on exit. The delivery step limits inheritance. The watch step limits the time window if something goes wrong before the process exits cleanly. In practice, "process exited" and "credential revoked" can be the same event if the credential lives only in the child's environment. But if the credential is registered with a broker that also controls access to remote resources, the broker needs to know the process is gone.

On Linux, prctl(PR_SET_CHILD_SUBREAPER, 1) makes a process the reaper for its subtree. Orphaned grandchildren that would otherwise become children of PID 1 become children of the subreaper instead. This lets a credential broker track the full subtree, not just direct children. Without it, a child process that forks and exits leaves grandchildren attached to PID 1, where the broker can no longer see them.

On macOS, kqueue with EVFILT_PROC and NOTE_EXIT delivers an event when a watched PID exits. You register the root PID and each child PID as the tree grows. The kernel delivers exit notifications in order. Portable code that runs on both platforms typically falls back to polling /proc/<pid>/stat on Linux when the subreaper approach is not available, but polling adds latency between exit and revoke.

Implementation realities

Three questions come up every time someone builds process-tree credential scoping.

When exactly does the grant end?

You have three options. Exit-triggered revoke ties the grant's end to the root process exiting. TTL-based expiry ends the grant after a fixed time regardless of process state. Explicit revoke lets the caller signal early termination. These are not mutually exclusive: a sensible design uses TTL as a ceiling, exit as the normal trigger, and explicit revoke for cases where the process is stuck or unresponsive.

The worst design omits all three and relies on the user to remember to clean up. That is shell-session scope with extra steps.

How do you handle credential inheritance across exec without leaking into siblings?

If you use environment variables as the delivery mechanism, every child of the root process inherits the value. Most of the time that is what you want: stripe trigger runs stripe-cli, which makes HTTP calls; all of those need the same credential. The problem is when the root process is a shell that spawns multiple unrelated commands in sequence. If bash is the root, every command in that bash invocation shares the credential. That is session scope again.

The fix is to make the root process as specific as possible. Do not scope to a shell; scope to a command. If you must scope to a shell for a short script, keep the shell script short and exit it when the work is done.

What does the audit log need to record?

At minimum: the credential identifier (not the value), the PID of the root process, the command that ran, the wall-clock time of grant start, and the wall-clock time of grant end. "Did the agent touch the prod Stripe key between 2 and 4pm Tuesday?" should be answerable in one command. If the log records only "grant issued" with no end event, you cannot answer that question.

An append-only log with a chain of cryptographic hashes (HMAC-chained entries) makes the log tamper-evident. An attacker who compromises the system after the fact cannot rewrite the record of what the agent accessed without breaking the chain.

Where it breaks

Process-tree scoping is not airtight. Three failure modes matter.

Double-fork daemons. A process that daemonizes itself calls fork(), lets the parent exit, then calls fork() again and lets the first child exit. The grandchild becomes a child of PID 1 and has no connection to the original process tree. Any credential the grandchild inherited from the original root is now outside the monitored subtree. If the credential broker tracks only the original subtree, the broker thinks the grant ended when the first-fork parent exited. The grandchild still holds the env var.

This is a real limit. If you are scoping credentials for commands that might daemonize, use TTL-based expiry as the fallback. The daemon lives longer than the credential's TTL. When the TTL fires, the broker revokes access to whatever the credential gates.

Process exit before the work completes. Suppose the root process exits cleanly but a background job it spawned is still running. On Linux without PR_SET_CHILD_SUBREAPER, that background job attaches to PID 1. The broker sees the root exit and revokes. The background job continues with a now-invalid credential. The next call fails. This produces confusing errors: the job succeeds for a while, then fails mid-run.

Detect this early. If your command spawns background jobs intentionally, make the root process wait for them before exiting, or model the background job as its own scoped grant.

Audit lag. On a busy system, the kernel delivers EVFILT_PROC exit events asynchronously. Between process exit and broker revoke, there is a small window. For most threat models this is irrelevant; an attacker who can exploit a millisecond window already has serious access. For compliance requirements that demand "no active grant after process exit," document the window explicitly and measure it in your environment.

One more thing the optimistic version of this model gets wrong: process-tree scope does not prevent the credential value from being written to disk. If the agent writes it to a log file, a state file, or a temp file that outlives the process, the credential escapes the tree entirely. Delivery mechanism scope and persistence scope are different problems. Fixing delivery does not fix persistence.

Checklist: process-tree scoped credential grants

## Process-tree credential scope audit

- [ ] No credentials exported as shell-session env vars (check ~/.zshrc, ~/.bashrc, ~/.profile)
- [ ] Credentials delivered only at exec time, scoped to the specific command
- [ ] Root process is a command, not a shell (no bash as the scope root)
- [ ] On Linux: subreaper or EVFILT_PROC equivalent tracks all descendants
- [ ] On macOS: kqueue EVFILT_PROC + NOTE_EXIT registered for root PID
- [ ] Audit log records: credential ID, root PID, command, grant start, grant end
- [ ] TTL set as ceiling even when exit-triggered revoke is the primary mechanism
- [ ] Double-fork / daemonizing commands identified and excluded from tree scope
- [ ] Explicit revoke path exists for stuck or unresponsive processes
- [ ] Credential value not written to any log, state file, or temp file inside the process
- [ ] Audit log is append-only and tamper-evident (HMAC chain or equivalent)
- [ ] "Grant end" events present in log for every "grant start" event

What this means for your stack

If your AI coding agent runs with shell-session credentials today, the Replit mid-2024 incident is a reasonable model for what can go wrong. The agent is not malicious. It uses what is available. The fix is not better prompting.

The runtime model you want: credentials live in an encrypted local vault. When the agent needs a credential to run a command, it requests a grant scoped to that command. The broker delivers the value into the command's environment at exec time and watches the process tree. When the root process exits, the grant ends. The audit log records the full picture. Nothing about that model requires the credential to live in the agent's context window or in any file the agent writes.

hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and wrap the next agent command with hasp run to get process-tree scoped delivery with an HMAC-chained audit log. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

The pattern holds regardless of tool. Scope is a property of a grant, not of a secret. The same credential can be session-scoped or command-scoped depending on how you hand it to the process. The narrower the scope, the smaller the blast radius when an agent does something it should not.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index