Skip to content

Corporate launcher

Force every process Claude Code spawns from its own binary, including background agents, through a mandatory company launcher with processWrapper.

Some security teams insist that nothing runs on a workstation unless it starts through a company launcher: a script that enters a sandbox, applies network policy or injects credentials. A wrapper around claude on your PATH covers the sessions people start by hand, but it misses a lot. The background service behind agent view, every session it hosts, and Claude Code's own relaunches after an update all start from the binary's direct path without consulting PATH.

CLAUDE_CODE_PROCESS_WRAPPER (and the equivalent processWrapper setting) closes that gap. Set it to your launcher's absolute path and Claude Code runs the launcher, passing the real Claude Code command as arguments, for every process it starts from its own binary.

Note: The environment variable needs v2.1.208 or later; the processWrapper setting needs v2.1.210 or later. Older versions silently ignore them and start everything unwrapped, so always run the verification step.

What is covered

With the launcher configured, these go through it:

  • The background service that claude agents and background sessions start on demand.
  • The terminal host and the session inside every agent view row, including the warm standby sessions the service keeps ready.
  • Sessions the service respawns after an update or crash.
  • Claude Code's self-relaunch to finish an update, including agent view's restart-for-update.
  • Session processes started by Remote Control (v2.1.210+).
  • Split-pane teammates that agent teams open in tmux or iTerm2 (v2.1.210+).

Windows is not covered. The launcher contract relies on exec, which Windows lacks, so the variable is ignored there. Processes run unwrapped, and the only clue is a warning in the debug log. If your policy covers Windows, count those machines as unwrapped.

What still starts outside it

  • An installed service written before the launcher existed. launchd or systemd starts it from its unit file. /status and claude daemon status warn about the mismatch; once the service restarts with the setting, its sessions are wrapped.
  • Sessions you start in a terminal. These run however you invoked them. Cover them with a script called claude earlier on your PATH that calls your launcher with the real binary. Do not replace the managed symlink. Because the service does not look up PATH, the two launchers do not stack.
  • The first process of a claude-cli:// deep link, which the OS protocol handler starts. Everything it spawns afterwards is wrapped. To close this path, set disableDeepLinkRegistration (see deep links).
  • --worktree with --tmux, whose pane the multiplexer starts.
  • The Claude in Chrome native-messaging host, which the browser starts.

Side effect worth knowing: with a launcher in place, ps and Activity Monitor stop showing the claude bg-pty-host and claude bg-spare labels, because exec rebuilds the argument list. Nothing is hidden; Claude Code tracks its processes by binary path.

Setting it up

1. Write the launcher

It must be executable, live at an absolute path, and finish by replacing itself with Claude Code. A realistic example that loads short-lived proxy credentials from a cache and enforces an egress policy:

#!/bin/sh
# /opt/acme/bin/claude-launch
# Pull a cached proxy token (refreshed elsewhere, so this stays fast)
if [ -r "$HOME/.cache/acme/proxy-token" ]; then
  ACME_PROXY_TOKEN=$(cat "$HOME/.cache/acme/proxy-token")
  export ACME_PROXY_TOKEN
fi
# Apply the egress profile, then become Claude Code
exec /opt/acme/bin/egress-guard --profile developer -- "$@"

If egress-guard itself execs its trailing arguments, the chain still ends with Claude Code replacing the launcher. Then chmod +x /opt/acme/bin/claude-launch.

Warning: If you previously replaced the ~/.local/bin/claude symlink with a launcher, restore the original symlink in the same change. Otherwise the first wrapped session starts the service through both launchers, and the install becomes "externally managed": /doctor complains, auto-update leaves the file alone and old-version cleanup stops.

2. Configure it in settings, not the shell

The background service starts on demand, outlives your shell and never reads shell profiles, so an export is not enough. Use the env block of ~/.claude/settings.json for one machine, or managed settings for the fleet:

{
  "env": {
    "CLAUDE_CODE_PROCESS_WRAPPER": "/opt/acme/bin/claude-launch"
  }
}

Or, if you push settings as individual keys:

{
  "processWrapper": "/opt/acme/bin/claude-launch"
}

Rules of precedence:

  • The environment variable beats processWrapper if both are set.
  • A managed value overrides ~/.claude/settings.json and shell exports, so users cannot swap in a different launcher.
  • Project and local settings can never configure it. A repository must not be able to put a binary in front of every Claude Code process, so the variable is ignored there (with a debug-log warning) and the key is never read from those files.
  • Delivered through server-managed settings, processWrapper appears in the security approval dialog alongside other settings that run admin-supplied executables.

3. Restart the service and sessions

A running service and open sessions read the value once at startup. Stop the on-demand service with claude daemon stop --any; the next claude agents or --bg starts a wrapped one. An installed service takes claude daemon stop without --any. Then restart open sessions.

On machines you cannot touch, the first new session after the settings push retires a leftover unwrapped on-demand service automatically. A machine where nobody starts a session keeps the old service until someone does, and installed services always need a manual restart.

4. Verify

In a session, /status has a Self-exec entry showing the resolved launch command, with a warning if the running service does not match. claude daemon status prints the same from the shell, and keeps doing so after you unset the variable.

The launcher contract

If the launcher cannot run, Claude Code refuses to start the process rather than run it unwrapped (except on Windows, as above). Your script must:

RuleWhy
End with exec "$@" (or exec something that execs it)A launcher that forks and exits orphans Claude Code. Agent view marks that session failed, names the launcher and cleans up.
Leave arguments aloneThe first argument is the binary and the rest is its argv. Do not reorder, drop or prepend.
Pass the whole inherited environment throughSession auth tokens, provider and model selection and the wrapper variable itself travel in the environment. Rebuilding it from an allowlist breaks sessions and shows as a launcher mismatch in /status. Adding variables is fine. If you enter a namespace that resets the environment, re-export everything inside it.
Reach exec within about three secondsA cold background dispatch runs the launcher twice in a row before any output. Do slow work like SSO exchanges lazily or from a cache.
Cope with running inside itselfEvery nested self-spawn is wrapped too, so if the launcher takes an exclusive lock it must notice it already holds it.
Print nothing before execAny output is reported as the crash cause if the session dies early.
Not depend on argument spellingA flag value might arrive as --flag value or --flag=value, and that can change between versions.

Value format

For most launchers the value is just a path. To give the launcher its own arguments, add them after the path. The value is parsed as an argument list, not a shell command:

  • Whitespace separates tokens; double quotes group a token containing spaces.
  • A value starting with [ is read as a JSON string array, for example ["/opt/acme/bin/claude-launch", "--tier", "contractor"].
  • No shell features: no variable expansion or globbing, and an unquoted ;, |, & or $( is rejected as a configuration error.

When the value cannot be used, Claude Code refuses to start the affected process and reports why; the errors page lists the launcher messages.

Not the same as CLAUDE_CODE_SHELL_PREFIX

These two are easy to confuse:

CLAUDE_CODE_PROCESS_WRAPPERCLAUDE_CODE_SHELL_PREFIX
WrapsClaude Code's own processesShell commands Claude runs for you: Bash tool calls, hooks, stdio MCP server start commands
Receives the command asSeparate argv tokens to execOne shell-quoted string in $1 to re-evaluate

A script written for one will not work as the other. See environment variables.