Root, plugins and MCP trust¶
Three admin decisions on an environment: whether the agent gets root, and which Claude Code plugins it runs. Then which of its MCP servers can carry text nobody you trust wrote.
Root in the sandbox: sudo¶
An environment spec's sudo gives the sandbox user passwordless sudo, which makes the agent root whenever it likes.
It is admin-set; a repo's own file can't set it. It is false by default (#124). With sudo the agent can rewrite
files hangar owns (managed harness settings), and the uid split protects nothing.
- Install at build time.
tools.apt, mise andsetupsteps run as root in the image build. So the shipped environments (base,bare,pythonand the rest) need no runtime root. A repo'sinstallruns as the sandbox user and should put what it installs inside the repo (.venv,node_modules). sudo: trueis for an environment whose people need root at runtime (apt-get installwhile exploring). Keep it for people's own sessions.- On Docker, a job's sandbox runs with
no-new-privilegesunderHANGAR_INTAKE_POSTURE=enforce(intake posture). So sudo fails there even withsudo: true, as it always does on GKE (allowPrivilegeEscalation: false). - Heavy environments run dockerd as root, so
heavy: true(ortier: heavy) impliessudo: true. An explicitsudo: falsebeside it is refused. - Under
enforce, an intake job on a heavy environment is refused unless the repo policy setsallow_heavy_for_intake: true(hangar repo set owner/repo --allow-heavy-for-intake). hangar doctorlists the environments withsudo: trueand whether intake jobs run in them. A sandbox's manifest recordspermissions.sudoandpermissions.no_new_privs.
Claude Code plugins¶
An environment's plugins: list names Claude Code plugins from local marketplace directories next to its spec. The
image build installs them with the locked Claude Code into a read-only seed at /opt/hangar/claude-seed. That seed is
root's, out of the agent's reach. hangar's own settings enable exactly those, in every turn, hardened ones included.
Only an admin's environment can declare them (not a repo's file), a prebuilt image: can't have them, and changing a
plugin rebuilds the image. The details, and what the manifest records, are on plugins.
A plugin can also come from a git repo, pinned by commit and fetched through the git gateway (git marketplaces).
harnesses: [claude-code]
plugins:
- {path: plugins/team, name: workflow} # .claude-plugin/marketplace.json in plugins/team
MCP servers and trust¶
An environment's mcp: servers are written into each harness's own config at claim
(environment specs). Each server can say whether its tool results can carry
text nobody you trust wrote:
mcp:
github: # issue comments, PR bodies: anyone can write them
url: https://api.githubcopilot.com/mcp/
headers: {Authorization: "Bearer ${GH_TOKEN}"}
trust: untrusted # the default for a url server
docs-index:
command: docs-mcp # a local stdio server over your own repo
trust: trusted # the default for a command server
mcp.<name>.trust is trusted or untrusted. Unset, a remote (url) server is untrusted and a local stdio
(command) server is trusted. A call to an untrusted server's tool taints the turn (taint).
Mark a stdio server that relays third-party text (Jira, Slack, a mail or web reader) untrusted. Mark a remote one
you run over your own data trusted. hangar's own hangar-env server is trusted; the built-in browser
(computer.browser_mcp) is untrusted; a server the agent adds itself is untrusted. A repo's
.hangar/environment.yaml can't set mcp: at all.
mcp.<name>.write_tools lists the server's tools that change something, as fnmatch patterns of its tool names
(["create_*", "update_*", "add_*_comment", "merge_pull_request"]). Once a turn has read untrusted content, a call to
one needs a person under ask and deny-risky (steering).