Which real agent process is asking for approval?
Learn how to identify the real agent process across terminals, IDEs, shells, scripts, and task runners so reviewers approve a recognizable actor.

An approval prompt that says "Terminal" or "zsh" is usually too vague to support a real decision. Those programs may be part of the launch path, but they are often shared plumbing. A reviewer needs to know which agent run asked to act, what started it, and whether anything in that chain gives the request a durable identity.
The hard part is that a process tree answers several different questions at once. It can tell you who forked whom. It can expose a task runner that nobody expected. It can show that a graphical IDE started the terminal. It cannot, by itself, prove that a signed parent approved every line of code a child will execute. Good approval design uses the tree as evidence, then states the narrow claim it can support.
Approval belongs to an actor, not a terminal
The actor a reviewer approves is the process run that requested the action, interpreted in the context of its launch chain. A terminal window is a place where processes run. It is not automatically the responsible program.
That distinction sounds fussy until a team has several agents running from one shell. One developer opens a terminal in iTerm2, runs an interactive coding agent in one tab, starts a repository task in another, and leaves a background watcher running in a third. All three descendants can have the same terminal application somewhere above them. Calling each request "iTerm2" tells the reviewer almost nothing.
The opposite mistake is just as common. A reviewer sees node, python, or zsh and assumes that the binary nearest the request is the whole identity. That binary might be the interpreter for an agent package, a package manager hook, a temporary script generated by an IDE extension, or a wrapper added by a team tool. The name is technically accurate and operationally useless.
Use three labels internally, even if the approval card only shows two:
- Requester: the process that actually asked to use the protected channel.
- Execution chain: the relevant parent processes that explain how the requester got there.
- Recognizable authority: the nearest ancestor whose identity a reviewer can reasonably recognize and whose code signature can be evaluated.
Those labels solve a problem that many approval systems blur. The requester answers "which process made this call?" The recognizable authority answers "who or what organization did this run come through?" They are related, but they are not interchangeable.
For example, a request might look like this:
Code Helper (signed application)
└─ zsh -l
└─ npm run agent
└─ node ./tools/start-agent.mjs
└─ agent-cli --workspace /Users/maya/src/payments
If agent-cli makes an HTTP request, it is the requester. The shell and npm explain the launch path. The signed Code Helper process may be the best recognizable authority, provided it is actually in the live ancestry and not merely open on the desktop. An approval card that says "agent-cli, launched from Code Helper" is honest. One that says only "Code Helper wants access" throws away the information that would distinguish this run from another extension or task.
A process tree is evidence, not a statement of intent
A parent process proves that it created or inherited a relationship with a child. It does not prove that the parent understood the child's later arguments, prompt, repository instructions, or remote response.
That limitation matters when people use signing language carelessly. Apple describes a designated requirement as the mechanism macOS uses to determine whether code is the same code across versions. It generally incorporates an identifier and signing authority. Apple also makes the boundary clear: unsigned code has no durable designated requirement, and ad hoc local signatures do not provide stable identity across versions.
That is useful evidence for approval. It is not an endorsement of an action. A signed IDE can start an unsigned repository script. A signed terminal can run a downloaded binary. A signed task runner can pass hostile environment variables into a perfectly ordinary interpreter. The signature lets the reviewer recognize a code publisher. It does not sanitize the chain beneath it.
Treat the following claims differently:
| Claim | What supports it | What it does not support |
|---|---|---|
| "This request came from PID 81234." | The local process record | Who wrote the code or why it made the call |
| "This child was launched by this parent." | Parent PID and live ancestry | That the parent approved the child's current behavior |
| "This executable is signed by this authority." | Signature inspection and designated requirement | That the executable is benign or acting within intent |
| "This run began from this IDE or terminal." | An unbroken live ancestor chain | That the visible window initiated every descendant |
The consequence is practical. Do not collapse all four claims into a friendly app icon and a single sentence. A reviewer should be able to see which claim they are accepting.
When the chain is ambiguous, say so. "Started by an unsigned script from a terminal session" is a better approval label than attributing the request to the terminal application as though the terminal authored it. False precision trains people to ignore the card.
Inspect the live chain before designing the approval card
The fastest way to find the right actor is to inspect a real run while it is alive. Do this with the agent performing a harmless operation, because the chain can change when the work moves from startup into a task runner or child helper.
On macOS, begin with the requester PID if your gateway records it. This command prints the process, its parent PID, start time, executable name, and arguments:
ps -p "$PID" -o pid=,ppid=,lstart=,user=,comm=,args=
A representative result has this shape:
81234 80991 Tue Jul 21 14:08:31 2026 maya /usr/local/bin/agent-cli agent-cli --workspace /Users/maya/src/payments
Then walk upward. ps does not recurse for you, so a small shell function keeps the inspection repeatable:
ancestry() {
local pid="$1"
while [ "$pid" -gt 1 ] 2>/dev/null; do
ps -p "$pid" -o pid=,ppid=,user=,comm=,args=
pid=$(ps -p "$pid" -o ppid= | tr -d ' ')
done
}
ancestry "$PID"
The output is intentionally plain. You are looking for changes in kind: agent executable to interpreter, interpreter to package manager, package manager to shell, shell to terminal or IDE helper. Do not stop because you recognize a name. Continue until you reach a process that gives the reviewer meaningful provenance or until the local chain ends.
For a broad snapshot, this view helps when several descendants exist:
ps -ax -o pid=,ppid=,user=,lstart=,comm=,args= | sort -n
Use it for investigation, not for an approval prompt. A full process listing contains too much irrelevant detail and too little structure. Your implementation should collect it when needed, then reduce it to the requester, the immediate parent, the recognizable authority, and the intermediary hops that explain an otherwise surprising launch.
There are two timing traps here. First, process IDs are reusable after a process exits. Record the start time along with the PID, and resolve the chain at request time rather than trusting an earlier sample. Second, a task runner can fork a worker and exit before the protected call occurs. If the parent is already gone, preserve the observed chain from process creation or report the missing link. Do not replace it with a guessed parent.
VS Code changes the launch path without becoming the requester
VS Code's integrated terminal can inject arguments or environment variables when it launches supported shells to enable shell integration. Its documentation also says that automatic injection does not cover every setup, including some subshell, SSH, and complex shell cases. That is a good reminder that terminal UI metadata and Unix parentage are separate sources of evidence.
In a simple case, the chain looks like this:
Visual Studio Code
└─ terminal helper
└─ login shell
└─ agent-cli
The terminal helper exists because the IDE needs to manage a pseudoterminal and its child shell. The login shell may source a profile that changes PATH, activates a language runtime, installs shell hooks, or starts a project-specific script. Those details explain why the same command behaves differently in a standalone terminal and an IDE terminal.
The approval decision should not pretend those details do not exist. Show the agent's executable as the requester. Present VS Code as the launch authority only when it is in the current ancestry. Include the shell and task command in an expandable path when they materially distinguish the run.
A frequent failure looks like this:
Visual Studio Code
└─ zsh
└─ make test-agent
└─ sh -c ./scripts/bootstrap-agent
└─ python3 tools/agent.py
A naive implementation sees zsh and labels the request "VS Code terminal." Another sees python3 and labels it "Python." Neither helps a reviewer determine whether this is the known repository task or an arbitrary process launched from the same shell.
The useful statement is closer to this:
Requester: python3 tools/agent.py
Run path: make test-agent > scripts/bootstrap-agent
Launched from: Visual Studio Code integrated terminal
That label has a cost: it exposes that the request came from repository-controlled code. It should. If a reviewer trusts a signed editor but does not trust the current checkout, the card must leave room for that judgment.
Do not infer VS Code ownership from terminal escape sequences, environment variables, or a window title alone. They can be inherited, copied, or stale. Live parentage is stronger evidence. Process context that the IDE provides can enrich the display after you establish that ancestry, not replace it.
A standalone terminal gives context, not blanket authority
iTerm2, Terminal, and similar apps give a human a deliberate point of entry. If a developer opens a clean terminal, types an agent command, and immediately receives an approval prompt, identifying the terminal application is useful context. It says where the run began.
It still does not justify blanket approval for every child that happens to share that session. Shells are designed to create children, and long-lived tabs often outlive the reason they were opened. A command launched at 9:00 in a terminal tab can leave a worker running through lunch, after a cd, a branch switch, an environment change, and three unrelated commands.
Make the distinction visible:
Requester: agent-cli --workspace /Users/maya/src/payments
Parent: zsh -l
Interactive origin: signed terminal application
Session started: Tue Jul 21 14:08:31 2026
The interactive origin is supporting context. The session belongs to the actual process run. If the process exits, revoke the session approval even if the terminal tab remains open. A new agent process in the same tab is a new run, with new arguments and possibly a different executable on PATH.
This is where reviewer-recognizable identity gets awkward. The terminal app may be signed by a known publisher, while agent-cli is an unsigned executable installed in a project directory. The honest card should name both facts. Do not promote the terminal's signature onto the unsigned child as if the terminal had vouched for it.
The same issue appears with shell functions and aliases. A command that looks like agent can expand into a function that changes directories, loads an environment file, launches a package script, and finally starts another binary. Your process collector will only see the resulting children. If you want the typed command as optional context, capture it from shell integration or a shell hook and label it as user-entered command history, not as process identity.
Scripts and task runners create the most misleading chains
Task runners are popular because they turn a complicated local setup into a memorable command. That convenience often makes their process chains look more trustworthy than they are.
Consider this ordinary launch:
zsh -l
└─ just agent
└─ /bin/sh -cu 'npm run agent -- --mode write'
└─ npm run agent -- --mode write
└─ sh -c node ./scripts/launch-agent.js --mode write
└─ node ./scripts/launch-agent.js --mode write
└─ agent-cli --mode write
Every middle process has a legitimate purpose. just selects a recipe. /bin/sh interprets the recipe body. npm selects a package script and starts a shell. Node evaluates the launch script. Yet none of those names tells a reviewer whether the protected request came from a known agent run or from a different command that happened to use the same runtime.
Do not remove the middle layers from the audit record. They are exactly where an investigation will find a modified package script, a surprising pre or post lifecycle hook, an altered PATH, or a repository script that substituted another executable. Reduce them in the approval display only after you retain them in an immutable event record.
A sensible display rule is:
- Put the requester executable and arguments first.
- Show the direct launcher when it changes the interpretation, such as
npm run deploy-agentormake migration-bot. - Name the first recognizable signed ancestor as launch authority.
- Keep the complete observed ancestry available in the event detail.
That rule avoids two bad habits. The first is hiding every wrapper, which makes a modified task invisible. The second is showing a seven-line tree before the reviewer can answer the request. People faced with unreadable approval cards approve the shape of the card, not the action.
There is a recommendation I reject: approve the task runner rather than the agent because the task name is easier for humans to recognize. Task names are often repository text. Anyone who can change the checkout may be able to change what the task does. Use the task name as context, but bind an approval session to the observed agent process run and re-evaluate when a new run starts.
Choose the signed boundary carefully
The nearest signed ancestor is usually the best recognizable authority, but "nearest" needs judgment. A signed system shell or interpreter may sit directly above an unsigned script. Calling that shell the authority produces a technically signed label that says nothing about who supplied the code.
Apple's code-signing guidance distinguishes an executable's identifier from its signing identity and uses a designated requirement to establish code identity. The codesign tool can display that requirement. Use the output to identify a signed app or helper, but do not treat an identifier string alone as proof of a publisher relationship.
For a bundled application, inspect the executable or app bundle that appears in the ancestry:
codesign --display --verbose=4 --requirements - "/Applications/Example.app" 2>&1 \
| sed -n '/Identifier=/p;/TeamIdentifier=/p;/Authority=/p;/designated =>/p'
A typical result contains fields in this form:
Identifier=com.example.editor
TeamIdentifier=AB12CDE345
Authority=Developer ID Application: Example Software (AB12CDE345)
designated => anchor apple generic and identifier "com.example.editor" and certificate leaf[subject.OU] = "AB12CDE345"
The exact fields vary with the signature and distribution path. What matters is that your approval system captures a stable code identity when macOS can provide one, then renders a reviewer-friendly version without hiding the raw evidence.
Use these rules when choosing the displayed authority:
- Prefer a signed graphical app or dedicated signed helper that is actually in the requester ancestry.
- Do not use a generic signed interpreter as a substitute for the unsigned script it executed.
- Do not infer authority from an executable's directory, repository name, or shell prompt.
- Mark unsigned and ad hoc-signed stages plainly instead of burying them below a recognizable parent.
- If no recognizable signed authority exists, say that the run has no verified local publisher identity.
A process can have several signatures in a conceptual sense: the app bundle signature, a nested helper's signature, and an interpreter's signature. Review the executable that actually appears in the process list. A signed app can launch a separately signed helper. A helper can launch a user-installed binary. Each is a separate identity question.
Some launch chains should lower confidence
A clean parent-child chain is not always available, and the missing cases are not edge trivia. They are where approval systems tend to make their worst attribution errors.
Remote execution breaks local ancestry
If a local agent invokes SSH, your Mac can identify the local SSH client and its parents. The remote command has a distinct process tree on another host. Do not say that a remote agent-cli was launched by a local IDE unless you collect and verify evidence on that remote machine.
The local approval card can still be useful. It can name the local requester, the destination, the remote account if known, and the exact command passed to SSH. That is enough to decide whether the local agent may initiate the connection. It is not enough to establish remote process identity.
Detached work needs a new identity decision
A child can call setsid, double-fork, or ask a supervisor to continue work after its original launcher exits. Once that happens, an approval tied only to the terminal or initial shell becomes fiction. The detached worker needs its own process record, its own start time, and a fresh approval scope unless you have a deliberately designed supervisor relationship that reviewers can inspect.
Environment inheritance can change the actor without changing the tree
The same node path can load a different agent package because PATH, NODE_OPTIONS, language-specific module paths, proxy settings, or working directory changed. Process ancestry does not show every inherited value. Capture a restricted context fingerprint for investigation: executable path, arguments, current directory, selected environment names and values after redaction, and a digest of the executable when that is appropriate for your model.
Do not dump an entire environment into an approval card or journal. Environments often carry tokens, endpoints, and personal paths. The goal is to distinguish runs, not to manufacture a second secret store in logs.
A reviewer needs a short claim and inspectable evidence
An approval prompt has seconds to earn attention. Put the decision-relevant claim at the top, then give a reviewer a path to inspect why the system made that claim.
A practical card might read:
Agent process requests an HTTP call
Requester
agent-cli --workspace /Users/maya/src/payments
PID 81234, started 14:08:31
Launched through
npm run agent > node scripts/launch-agent.js
from a signed editor process
Destination
api.example.internal / POST /deployments
That is compact enough to read and specific enough to challenge. A reviewer can reject it because the workspace is wrong, the task runner is unexpected, the destination is surprising, or the request came from an unsigned stage. Those are real reasons to stop an action.
Keep the full evidence behind the card. Preserve the requester PID and start time, executable path, arguments, parent chain, signing facts for the selected authority, and the approval decision. If the same agent later makes another request in the approved run, the record should make it clear that the session decision applied to that process lifetime, not to an app name floating free of a process.
Sallyport's per-session authorization is built around that practical boundary: the first call from a new agent process asks for approval, and the approval lasts only until that process exits. Its approval card leads with the process's code-signing authority, which gives a reviewer a recognizable starting point without pretending that the signature explains every child command.
Revoke by process lifetime, then make exceptions explicit
A session approval should follow the agent process lifetime because the process is the unit that made the request. Reusing approval because a terminal remains open, an IDE window remains visible, or a task has the same name creates an approval scope nobody can inspect honestly.
This rule does create friction for short-lived wrappers. A package manager may start a new agent process for every subcommand. Do not fix that by quietly widening approval to every future child of npm or every process in a terminal. Decide whether the workflow needs a stable, signed agent host, a longer-lived process that exposes one clear identity, or per-call confirmation for the sensitive credential.
Per-call approval is the right answer when the risk is in each use rather than the launch identity. A production deployment token, an SSH private key that reaches a sensitive host, or a write-capable administrative API may deserve a human decision every time even when the process identity is familiar. Process recognition reduces confusion. It does not eliminate judgment.
The implementation test is simple: kill the approved process, start the same command again, and confirm that the second run needs its own approval. Then start a different command in the same terminal tab and confirm that it cannot inherit the first run's decision. If either test fails, the system approved a location or a label instead of an actor.
The reviewer should never have to trust a terminal name as a substitute for process identity. Preserve the chain, identify the requesting run, show the strongest recognizable authority you can defend, and let missing evidence remain visible. That is less glamorous than a broad "trusted IDE" rule. It is also what holds up when a task script, shell wrapper, or remote hop turns out to be the part that mattered.
FAQ
How do I identify the real process behind an AI agent request?
Start with the process that made the protected request, then walk upward until you reach a boundary a human can recognize, usually a signed application, terminal app, IDE, or managed runner. Do not label the request with the nearest shell merely because it is easy to display. The shell often explains how the request started, but it may not be the program that decided to make it.
Is the terminal app the identity I should approve?
A terminal application can be a useful part of the identity, especially when a developer deliberately launched a one-off run there. It is not sufficient on its own when several unrelated tools share the same terminal session. Include the terminal as launch context, then show the agent executable and the relevant parent chain.
What should happen when an agent is launched by an unsigned script?
An unsigned script does not give a reviewer a durable publisher identity, and a filename is trivial to copy. Show the script as execution detail, but attach approval to a signed ancestor when one exists, plus the interpreter and working context. If the chain has no recognizable signed authority, treat that as less trustworthy context rather than inventing certainty.
Does a VS Code integrated terminal prove that VS Code launched the agent?
VS Code can launch supported shells with injected arguments or environment variables for shell integration, so terminal decorations are not a clean statement of who owns every child process. Inspect the actual parent process IDs instead of inferring identity from the integrated terminal UI. A child launched there may belong to an extension host, a task, or a shell command the user ran manually.
Can npm, Make, or a task runner hide the real agent process?
No. A task runner may invoke a package script, which invokes a shell, which invokes an interpreter, which finally starts the agent. The useful answer is a compact chain that preserves those hops while identifying the first recognizable authority above them. Hiding the middle layers makes incident review much harder.
Does code signing prove an agent action is safe?
A code-signing authority tells you who signed a program and whether macOS can evaluate it as the same code across updates. It does not tell you whether the program's current prompt, repository, arguments, or remote instructions are safe. Treat signing as a stable identity clue, not a safety verdict.
What information belongs on an agent approval card?
A reviewer should see the requesting executable, its process ID, its immediate parent, the recognizable signed ancestor, the launch path, and enough command detail to distinguish one run from another. Show the working directory and start time when available. Do not make a person hunt through a full process table during an approval decision.
How should reviewers handle agents started through SSH?
A remote SSH hop breaks local ancestry. Your machine can identify the local client process and the process that launched it, but it cannot honestly identify the remote program from the local process tree alone. Record the destination and the local actor, then require separate evidence on the remote host if that identity matters.
What happens if the approved agent spawns another process?
A process can fork, reparent, daemonize, or pass work to another service after the original launcher exits. Session-scoped approval should end when the approved process run ends, and a later independent process should need a fresh decision. Long-lived helpers need their own recognizable identity and a clear explanation of why they persist.
Should every approval show the full process tree?
Use a process tree when you are diagnosing a launch chain, investigating an unexpected request, or designing approval UI. It is unnecessary for every ordinary approval if the system has already condensed the chain into a clear actor label and supporting details. The tree is evidence for review, not a thing every reviewer should have to decode.