DEV Community

JasperNoBoxDev
JasperNoBoxDev

Posted on • Originally published at noboxdev.com

How We Built Process-Tree Agent Detection

How do you tell if a human or an AI agent is requesting a secret?

This question sits at the center of NoxKey's security model. An AI agent needs your Stripe key to make API calls. A human needs it to paste somewhere. Both call noxkey get. But the response should be fundamentally different — because what happens after delivery depends entirely on who's asking.

A human uses the value and moves on. An agent ingests it into a conversation context where it can be logged, echoed in debug output, included in generated code, or stored in a chat history on someone else's server. Same secret, wildly different risk profiles.

We spent two weeks building the process tree detection system that powers NoxKey's agent access control. Here's exactly how it works, where it breaks, and why imperfect detection still beats no detection at all.

CLI
noxkey get
Detection -->

Agent Detection
Process Tree Walker
Dual Verification
Menu Bar -->

Unix Socket

Menu Bar App
Server + Touch ID
Keychain -->

Keychain
Encrypted

Every process has a family tree

Every process on macOS has a parent. Your shell was started by Terminal.app. Terminal.app was started by launchd. When you type a command, your shell forks a child process to run it. This chain — child to parent to grandparent — is the process tree.

When Claude Code runs a command, the chain looks like this:

launchd PID 1    └─ claude ← Electron app (MATCH)      └─ node ← Claude Code runtime        └─ zsh ← spawned shell          └─ noxkey ← get org/proj/STRIPE_KEY

When a human runs the same command from Terminal:

launchd PID 1    └─ Terminal.app      └─ zsh ← login shell        └─ noxkey ← get org/proj/STRIPE_KEY

The difference: one tree has a process named claude. The other doesn't. That's the signal.

Walking the process tree in Swift

NoxKey's server is a native Swift menu bar app. The process tree walker uses macOS kernel APIs — specifically proc_pidinfo and sysctl with KERN_PROC — to climb from any PID to launchd.

The algorithm: start at the requesting process, get its parent PID, check the binary name, move up, repeat. Stop at PID 1 or on match.

func isAgentProcess(pid: pid_t) -> Bool {
    var currentPid = pid
    var depth = 0
    let maxDepth = 20  // safety limit

    while currentPid > 1 && depth average detection time


    1.6ms
    worst case


    20
    ancestors max depth


## The agent signatures list

Detection checks each ancestor's binary name against known AI coding tool signatures:

Enter fullscreen mode Exit fullscreen mode

private let agentSignatures = [
"claude", "cursor", "codex",
"windsurf", "copilot", "cody",
"aider", "continue", "tabby"
]




Case-insensitive substring matching. If any ancestor's binary name contains any of these strings, the caller is classified as an agent.

Could an agent rename its binary to bypass detection? In theory, yes. In practice, agent binaries are code-signed, distributed through Homebrew or app stores, and installed to standard paths. Users don't rename them. And if an agent vendor deliberately tried to evade detection, the headline writes itself: "Cursor caught disguising itself to bypass credential controls."

Name-based detection works because the incentives align. Agent vendors *want* to be identified. Being detected means getting the encrypted handoff instead of being blocked entirely. The alternative — no detection, no access — is worse for everyone.

## Dual verification: never trust the client

The CLI performs process-tree detection client-side. But the CLI is a binary on the user's machine. A malicious script could bypass it entirely and talk to the Unix socket directly, claiming to be human.

So the server verifies independently.

How dual verification works
  When the CLI connects to NoxKey's Unix domain socket, the server resolves the peer's PID using the `LOCAL_PEERPID` socket option — a kernel-level credential, not something the client sends. The server then walks *that* process tree independently.

Both sides must agree. If the CLI says "human" but the server sees `claude` in the ancestry, the server's verdict wins. The more restrictive interpretation always takes precedence. A compromised CLI can't downgrade its own classification.

Same principle as server-side validation in web apps. The client can lie. The server checks anyway.

## The encrypted handoff

When detection confirms an agent caller, NoxKey doesn't refuse the secret. It changes *how* the secret is delivered. This is the critical design decision: agents need secrets to function. Blocking them entirely just pushes developers back to `.env` files. Instead, we make the secret available to the agent's process without exposing the raw value in its text context.

The handoff sequence:

1. Generate Key ← random AES-256-CBC key + IV    └─ 2. Encrypt ← secret value encrypted with one-time key      └─ 3. Transmit ← payload + key + IV over Unix socket        └─ 4. Decrypt ← CLI decrypts via CommonCrypto          └─ 5. Write Script ← self-deleting temp script to /tmp (0600)            └─ 6. Output ← source '/tmp/noxkey_abc123.sh' to stdout              └─ 7. Cleanup ← file removed after 60s safety net

  - The server generates a random AES-256-CBC key and initialization vector
  - The secret value is encrypted with this one-time key
  - The encrypted payload, key, and IV return to the CLI over the Unix socket
  - The CLI decrypts using CommonCrypto (Apple's native crypto framework)
  - The CLI writes a self-deleting temp script to `/tmp` — containing `export KEY=value` followed by `rm -f "$0"`
  - The CLI outputs `source '/tmp/noxkey_abc123.sh'` to stdout
  - A background cleanup process removes the file after 60 seconds regardless

The agent runs `eval "$(noxkey get org/proj/STRIPE_KEY)"`. The eval sources the temp script, which exports the secret into the shell environment and deletes itself. The secret is now in `$STRIPE_KEY` — available to subprocesses — but the raw value never appeared in the agent's conversation context. It flowed through the OS, not through the chat.

The temp file exists on disk for milliseconds. Created with `0600` permissions (owner-only). The 60-second cleanup is a safety net for cases where the script isn't sourced.

## Defending against PID recycling attacks

PID Recycling Attack
  A legitimate process authenticates with Touch ID and gets a session. When it exits, macOS can recycle its PID. A new process inheriting that PID could hijack the authenticated session — accessing secrets without ever touching the fingerprint sensor. On a busy system with a 99999 PID space, recycling can happen within seconds.

NoxKey has session unlock: run `noxkey unlock org/proj`, authenticate with Touch ID once, and subsequent `get` calls under that prefix skip biometric auth for a configurable window. The session is bound to the PID that initiated it.

The attack scenario:

  - A legitimate process (PID 48201) calls `noxkey unlock org/proj` and authenticates with Touch ID
  - The session manager records: "PID 48201 has an active session for `org/proj/*`"
  - The legitimate process exits. PID 48201 is now free
  - An attacker spawns a new process. macOS assigns it PID 48201 — recycled
  - The attacker calls `noxkey get org/proj/DATABASE_URL` from PID 48201
  - The session manager sees the PID, finds an active session, skips Touch ID
  - The attacker gets the secret without ever authenticating

The fix: sessions are bound to PID *and* process start time. When a process calls `unlock`, the session manager records the PID and the boot-relative start timestamp from `kp_proc.p_starttime` (via `sysctl` with `KERN_PROC`). Every subsequent request checks both. A recycled PID has a different start time — microsecond precision makes collisions effectively impossible. The session check rejects it, and Touch ID is required again.

## Command-level blocking for AI agents

Process tree detection enables granular access control. When the caller is an agent, certain commands are blocked:

  - `--raw` — no plaintext stdout. Agents can't pipe raw values
  - `--copy` — no clipboard access for agents
  - `load`, `export`, `bundle`, `env` — no bulk secret operations

And certain commands remain available:

  - `ls` — agents can discover key names (no values shown)
  - `peek` — agents can see 8-character prefixes for verification
  - `get` — returns encrypted handoff, not raw values
  - `set --clipboard` — agents can store secrets from your clipboard

The CLI exits with a clear error: "This command is not available to AI agents." No ambiguity. The agent knows exactly why it was blocked and can tell the user.

## Honest limitations of process tree detection

This approach isn't perfect. We want to be upfront about where it breaks.

**Name-based matching has blind spots.** A new agent not in the signatures list won't be detected. We update the list with each release, but there's always a window. Obscure agents get treated as human callers.

**Detection is point-in-time.** It happens when the secret is requested. If an agent already has a secret in its environment from a previous session — before NoxKey was installed, or from a `.env` file it read earlier — detection can't revoke that access.

**This is macOS only.** The implementation uses `proc_pidinfo`, `sysctl`, and `LOCAL_PEERPID` — all macOS-specific APIs. The concept is portable to Linux (via `/proc`) and Windows (via `NtQueryInformationProcess`), but this code isn't.

**Sophisticated evasion is possible.** An attacker with root access could manipulate process names or inject into a legitimate process. But root access means your [Keychain secrets](https://noboxdev.com/blog/macos-keychain-for-developers) are already compromised regardless.

Despite these limitations — no other secrets manager distinguishes between human and agent callers at all. Every `.env` file, every `1password read` call, every `vault kv get` treats all callers identically. Imperfect detection that catches 95% of real-world agent access patterns is categorically better than zero detection. The [most common ways developers leak credentials](https://noboxdev.com/blog/credential-hygiene-for-developers) are all mitigated by this approach, even with its limitations.

We're not building an unbreakable wall. We're making the default safe.

When a developer installs NoxKey and an AI agent requests a secret, the right thing happens automatically — no configuration, no flags, no awareness required. That's the bar. Process-tree detection clears it.

Key Takeaway
  Process-tree agent detection uses macOS kernel APIs to walk from the requesting process to its ancestors, checking binary names against known AI coding tool signatures. Combined with dual verification (client + server via LOCAL_PEERPID), encrypted handoff delivery, and PID+start-time session binding, it catches 95% of real-world agent access patterns — without requiring any configuration from the developer.

---
*NoxKey is free and open source. `brew install no-box-dev/noxkey/noxkey` — [GitHub](https://github.com/No-Box-Dev/Noxkey) | [Website](https://noxkey.ai)*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)