Agent process authorization: revoke forked access
Agent process authorization needs a boundary that survives forks. Learn to inspect children, revoke access, stop process groups, and retain evidence.

Autonomous agents rarely stop as cleanly as their task logs suggest. An agent can finish a coding request, print a cheerful completion message, and leave behind a formatter, test runner, tunnel, shell, or helper process that continues to run. If that leftover process can still call production APIs or open SSH sessions, the task did not actually end where your interface says it ended.
The fix is not to become obsessive about killing processes. Process cleanup is necessary, but it is not an authorization system. Define the lifetime of authority separately from the lifetime of Unix processes, make the boundary observable, and revoke access before you begin chasing descendants. I have seen teams treat a parent PID as proof of containment. It is not proof of much once an agent can invoke a shell.
A process tree does not define an authorization boundary
A parent-child relationship tells you who created a process. It does not tell you whether that process should retain authority after the parent finishes. Those are different questions, and mixing them produces the familiar failure: someone cancels an agent task, sees the parent disappear, then discovers a child still making requests five minutes later.
Unix gives processes several ways to escape the shape you expected. A child can fork again. It can create a new session with setsid. It can ask a service manager to supervise it. It can leave a shell pipeline where only one member receives a signal. It can simply survive because the parent sent no signal at all.
A process also carries more than its command line. It may inherit environment variables, current working directory, open files, pipes, sockets, and file descriptors. If a parent holds a bearer token in an environment variable, any child that inherits that environment has the token. Killing the child later does not put that secret back in the vault, and it does not undo requests already made.
This is why an authorization boundary should answer a narrow question: which live process run may ask for privileged actions, until when, and how do we deny it immediately? The answer should not be "anything descended from the terminal that started the task." That is an accident of process layout, not a security decision.
For an agent that needs HTTP or SSH access, keep credentials outside the agent process. Let a credential-owning component perform the action after it recognizes an approved caller. That design changes cleanup from a desperate effort to erase leaked authority into routine operational hygiene.
Define the lifetime before you launch the task
An agent run needs a stated start condition, end condition, and revocation condition. Write those down before you decide whether a click in a UI, a process group, or a shell wrapper counts as control.
For interactive work, the sensible default is usually one authorization for one root agent process. The authorization begins when you approve that particular run and ends when the root process exits or you revoke it. A new agent invocation receives a new decision, even if it runs the same binary from the same directory.
For unattended work, do not quietly stretch that approval across the day because repeated prompts annoy people. Give the job a named owner, a finite duration, a limited set of destinations, and a cancellation path that someone can use while the job is still running. If the job needs to continue after its parent task ends, treat it as a separate job and make it request separate authority.
Three questions expose fuzzy designs quickly:
- Which executable and process instance did the user approve?
- What event ends its authority if the process never sends a clean completion signal?
- Can a descendant obtain a new privileged action after that event?
If the last answer is yes because the descendant inherited a token, you have delegated authority without recording the delegation. If the last answer is yes because the gateway still considers every descendant trusted, you have made process ancestry your policy language. Both choices become hard to explain during an incident.
Do not confuse convenience with a boundary. A terminal tab, a project directory, an agent account, and a code-signing identity are all useful context. None alone identifies one run. A code-signing identity tells you who signed an executable. It cannot tell you whether the executable launched an expected helper, a stale copy of a helper, or a child that detached after cancellation.
Inspect the live tree before you terminate anything
On macOS, start with the process facts that the kernel exposes instead of guessing from a task label. The ps manual documents pid, ppid, pgid, and sid as distinct fields. They show process ancestry, process-group membership, and session membership. You need all of them when an agent has been allowed to invoke shells and tools.
Run this command and save the output when you investigate a run:
ps -axo pid,ppid,pgid,sid,stat,etime,command
The useful shape looks like this:
PID PPID PGID SID STAT ELAPSED COMMAND
48102 47790 48102 48102 S 00:18:04 agent-cli run build
48131 48102 48102 48102 S 00:17:59 /bin/sh -c make test
48144 48131 48102 48102 S 00:17:56 test-runner --watch
48209 1 48209 48209 S 00:16:02 helper --upload-results
The first three processes share a group and session. The last process has PPID 1 and a different group and session. It may have detached, or a launcher may now own it. Either way, sending a signal only to PID 48102 will not stop it.
For direct children of a known root PID, use:
pgrep -P 48102 -alf
That command finds only one generation. Repeat it for each child if you need a quick manual walk. For an incident record, capture ps output before and after revocation, then record the exact root PID, start time, command, process group, and session. A bare process name is poor evidence because names repeat and command lines change.
Check open network connections when the risk involves external calls. On macOS, lsof can show a process's network files:
lsof -nP -p 48209 -i
A listening socket, an established outbound connection, or a long-lived SSH transport changes the urgency. It does not prove malicious behavior. It proves that a process you thought was gone still has a channel worth understanding.
Do not build a security control that depends on parsing human-readable ps output in production. Use it for investigation and testing. A real launcher should record identifiers at launch time and should hold a direct revocation handle at the authorization gateway.
Process groups help, but detached children defeat them
A dedicated process group gives a launcher a practical way to cancel a normal task tree. Create the group before the agent starts, keep the root as group leader, and send signals to the group rather than only to the root process. This catches the common case of shells, compilers, test runners, and pipelines that remain in the same group.
On systems that support the usual signal syntax, a negative group ID targets a process group:
kill -TERM -48102
sleep 3
kill -KILL -48102 2>/dev/null || true
The TERM signal gives ordinary tools a chance to close files and report cancellation. The later KILL signal handles processes that refuse or fail to exit. Do not copy this into automation until you verify that 48102 is the intended process group. A mistaken group ID can terminate your own shell or unrelated work.
This method has limits. A child can call setsid, which creates a new session and usually a new process group. A task can submit work to a local service, a remote build system, or a queue. A shell can start a background process outside the group. Once that happens, group termination becomes cleanup, not containment.
On Linux, a service manager can place a task in a dedicated cgroup and terminate the cgroup as a unit. That is usually stronger than process-group cleanup because the kernel tracks membership beyond ordinary parentage. Do not imply that a macOS menu-bar application has this control. macOS and Linux have different process supervision models, and a portable agent design should not pretend otherwise.
The recommendation to "just kill the process tree" remains popular because it works in demos. It fails under the exact conditions that make agent access risky: long jobs, background helpers, wrappers, and partial cancellation. Use process groups because they reduce debris. Do not use them as your only revocation mechanism.
Keep secrets out of the tree entirely
The safest child process is still one that cannot read a credential. Passing a token through an environment variable makes the token available to every descendant that inherits the environment, and it can also expose the token to diagnostics, crash reports, or careless logging. Passing a temporary file is only slightly less bad if the child can copy it before you remove it.
Avoid these patterns for agent-launched commands:
export DEPLOY_TOKEN='token-value'
agent-cli run deploy
agent-cli run deploy --token "$(cat ~/.config/deploy-token)"
Both put raw authority in the agent's execution environment. The second also risks placing it in process arguments, shell history, or logs. Rotating the token after a problem may be necessary, but rotation is a recovery action, not a normal cancellation path.
Use a local action gateway instead. The agent should request an operation, such as an HTTP request to an approved endpoint or an SSH command, without ever receiving the credential material. The gateway injects the relevant credential, executes the action, and returns the result. That gives you a place to deny the next call even if a stray child still runs.
Sallyport takes this approach for HTTP APIs and SSH: its encrypted vault stays in the app, while the agent connects through the sp mcp shim and receives action results rather than secrets. That matters more than any clever process-killing script because a child cannot inherit a token it never had.
Do not overstate this benefit. A process with an approved gateway session can still ask for actions until that session ends or you revoke it. Keeping secrets out of the process tree limits credential theft; it does not make an approved agent harmless.
Approval should attach to a run, not a family name
Authorization based only on an executable name is weak. Anyone can copy a binary to a different path, wrap it in a shell script, or run another instance later. Authorization based only on a signer is better for attribution, but it is still too broad if it silently approves every future run signed by the same party.
A usable approval card should identify the requesting process in terms a person can check: its code-signing authority, executable path, root PID, and start time. The decision should apply to that one process run. A child process should not obtain an endless right to act merely because it has an approved ancestor somewhere in its history.
There are two sensible models for descendants. The stricter model asks each distinct requester for approval. The practical model permits calls that occur during one approved root run, then rejects all new calls when that run ends. The second model works well for agents that legitimately launch short-lived tools, provided the gateway can tell that the root run has ended and the authorization cannot be reattached to a later process with the same name.
Sallyport uses per-session authorization by default: the first call from a new agent process presents an approval card that leads with the process's code-signing authority, and the approval lasts until that run exits. Its vault gate denies every action while locked, and a per-call setting can require approval for a particular credential on every use. Those are deliberately small controls. A pile of policy rules would only hide the question of who approved what.
Per-call approval fits credentials where each action deserves deliberate review, such as a production deployment account or a destructive administrative API. It does not fit every read-only request. People learn to approve anything when every harmless action demands attention. Put friction where the consequence warrants it, then make the session boundary short and clear elsewhere.
Revoke authorization before you chase the process
When a parent task ends unexpectedly, revoke its ability to make new privileged calls first. Then terminate the root group, inspect survivors, and clean up whatever escaped. Reversing that order creates a gap: the child you have not found can continue calling out while you inspect process tables.
A sound response sequence looks like this:
- Revoke the session or lock the vault at the action gateway.
- Preserve the root PID, process information, recent action records, and the cancellation time.
- Send
TERMto the known process group and inspect the remaining processes. - Escalate to
KILLonly for processes that still belong to the task and refuse to exit. - Check whether detached local processes or remote jobs require separate cancellation.
The first action must work even if the root PID has already vanished. It must also work if an agent deliberately tries to keep a helper alive. A gateway that requires the caller to be present for revocation has the direction backwards.
Instant revocation should deny future requests, not rewrite history. Keep the records that show the prior authorization and calls. If a gateway logs only successful actions, it hides useful evidence during an investigation. Denied calls after revocation tell you that something continued trying to act.
Vault locking is the emergency brake for every active session, which is appropriate when you cannot identify the compromised run quickly. Session revocation is the narrower response when you can. Keep those operations distinct so an operator does not have to choose between doing nothing and interrupting every engineer's work.
An audit trail must join the process evidence
An activity record without process context answers only half the question. It may say that an HTTP request occurred, but not which approved run initiated it. A session record without individual calls has the opposite problem. You need both views and a way to verify that someone did not quietly edit them after an incident.
Record at least the authorization event, the requesting process identity, session start and end, every privileged action, revocation, and any denial after revocation. Include timestamps and stable correlation identifiers. Do not record raw secrets. Do not assume a command line is safe to store either, because command lines often contain values that should never have been passed there.
Sallyport projects its Sessions and Activity journals from one encrypted, hash-chained audit log. Its sp audit verify command checks the chain offline over ciphertext without needing a vault key. That verification is useful when you need to hand an exported record to someone who should confirm integrity but must not read stored credentials.
A hash chain does not make an incomplete log complete. If your launcher never recorded the root PID, the audit record cannot reconstruct it later. If a remote job received an API request and continued on another machine, local process evidence will not show the remote process. Audit the request that initiated remote work, then require the remote system to expose its own cancellation and event records.
A familiar failure has two separate fixes
Consider a coding agent asked to run integration tests and publish a report. It starts a shell, which starts the test runner, which starts a helper to upload results. The user sees a test failure and cancels the agent. The parent agent exits. The shell is gone. The helper has already detached, retains an outbound connection, and sends a report after cancellation.
If the upload credential lived in an environment variable, the helper may possess it even after you revoke local approval. You now need credential rotation, log review, and possibly an incident response. Process cleanup arrived too late because the authority had already crossed into the child.
If the helper requested its upload through a gateway, revoking the parent session stops a new upload request. If the upload had already begun, the gateway audit shows that fact. You still terminate the helper, but you no longer rely on termination as the only means of control.
The awkward case is a helper that must outlive the agent by design, such as a local preview server or a queued release job. Do not call it a child and inherit authority forever. Give it an explicit owner, a separate authorization record, a defined expiry, and a visible stop control. Once work outlives its initiating task, it has become its own operational object.
Test cancellation against a stubborn descendant
A control you never test will fail in the least convenient way. Build a harmless test agent that launches a child which sleeps, opens a harmless local connection, and attempts a gateway action after the parent exits. Then cancel the parent at several points: before the child starts, while it runs, after it detaches, and while it has an action in flight.
Your expected result should be specific. The normal child exits with the process group. A detached child may remain visible, which proves why you still inspect. After revocation, every new privileged request receives a denial. The session record shows the approval and revocation, while the activity record shows calls before and after the boundary.
Do not let a passing test mean only that the UI changed state. Verify the operating system process state, the gateway decision, and the audit record. Those are three different observations. A cancellation button that only hides a task card is theater.
The first practical change is simple: record a root process identity whenever you authorize an agent, and make the authorization expire independently when that run ends. Then run the stubborn-child test before you trust a cancellation feature with credentials that can change production.
FAQ
What is a forked AI agent process?
A forked agent process is a child process created by an agent or one of its tools. It can keep running after the parent task reports completion, and it may retain open network connections, inherited file descriptors, environment variables, or access to a credential broker.
Does an agent child process stop when its parent exits?
No. A parent exiting changes process ancestry, but it does not reliably terminate descendants. A child can ignore a polite shutdown, detach into another session, or be adopted by the operating system's service manager.
How can I find child processes started by an agent?
Use ps to inspect PID, PPID, process group, session, elapsed time, and command, then use pgrep -P for direct children. On macOS, ps -axo pid,ppid,pgid,sid,stat,etime,command gives more useful evidence than relying on a graphical activity view.
Can I kill an entire process group safely?
A process group is useful when the task launcher creates it before the agent starts. You can send a signal to the negative process group ID, but that only works if children stayed in that group and did not deliberately create a new session.
Should I revoke agent access before killing its processes?
They solve different problems. Killing a process stops future work but may leave a short window and does not explain what happened. Revoking authorization stops new privileged actions at the gateway, which is the safer first move when a process may still be alive.
Is code signing enough to authorize an autonomous agent?
Code signing identifies who signed an executable, not what every process it launches will do. Treat it as useful attribution for an approval screen, then bind the approval to one observed process run and make its expiry unambiguous.
Can a child process inherit API credentials from its parent?
Shell variables, exported tokens, configuration files, temporary files, open sockets, and inherited descriptors can all carry authority farther than intended. Avoid passing raw secrets to the agent process at all, because process cleanup cannot reliably retrieve data it has already read.
How long should an agent authorization last?
Approvals should end when the authorized process run ends, even if a descendant remains alive. A child that needs continued access should ask through its own explicit boundary rather than silently inheriting the parent's approval.
Does an audit log prevent a rogue agent from acting?
A tamper-evident audit log can prove that records were not altered without detection, but it does not prevent an unwanted call. You still need a vault gate, an approval boundary, and a way to cut off a live run.
What is the first control to add for long-running agent tasks?
Start by launching each agent task in a dedicated process group, recording its root PID and start time, and testing cancellation against a deliberately stubborn child. Then make revocation independent of process cleanup, because process cleanup will eventually fail when you need it most.