Skip to content
T threat.cafe

· Tobias Lindqvist-Rao (editorial persona) · Agent artifact supply chain · on threat.cafe

Skills inherit everything your agent can touch: scoping credentials and shell access per skill and MCP server

An installed skill gets whatever the agent process can reach: its environment variables, the files its user can read, and its network route. A local MCP server gets the same, because it is a child process of the agent. Least privilege for skills therefore cannot live in the skill's description or in the prompt. It has to live in three places the operating system or the agent client enforces: which credentials the process environment holds, which commands may run, and which hosts the network path admits. This article maps those controls for Claude Code as documented in October 2026 and gives a capability matrix template for the rest of your stack.

The symptom: one approval, the whole environment

The Cloud Security Alliance post republished from Akto in June 2026 states the problem plainly: "Skills inherit the agent's full runtime context." If the agent can reach a GitHub token, AWS credentials or a database connection string, every installed skill can reach them too. The same post notes there is "no per-skill permission boundary in most deployments."

Scale matters here. The Skillselion census of 12 August 2026 covered 79,848 listings and found that installs of shell-capable skills are "at least 43.5%" of the total, a floor because two thirds of listings had not yet been parsed. Those permissions are what each skill's documentation declares, not a sandbox measurement. Even as a floor, it means a large share of installed skills expect to run shell commands.

Anthropic's own enterprise guidance for Agent Skills rates bundled scripts as high risk because "scripts run with full environment access."

Topology: what actually inherits what

For a Claude Code session on a developer laptop, the process tree looks like this:

claude (your user, your env vars, your ~/.aws, ~/.ssh, ~/.npmrc)
 |-- Bash tool commands          -> sandboxed if sandbox.enabled
 |     `-- skill scripts, npx, curl, package installs
 |-- skill !`command` injection  -> checked against permission rules
 |-- local stdio MCP servers     -> outside the sandbox, full user access
 |-- hooks, status line, apiKeyHelper -> outside the sandbox
 `-- Read / Edit / WebFetch tools -> permission rules, not the sandbox

The Claude Code sandboxing documentation is explicit about the boundary. The sandbox "covers shell commands only," and "local MCP servers ... run with your full access." Inside the sandbox, environment variables are "Inherited from Claude Code, including any secrets in its environment," and the default read scope is "Most of the machine, including credential files such as ~/.ssh and ~/.aws/credentials."

That last line is the root of most skill risk. The sandbox's default posture restricts writes and network. It does not hide your secrets from reads.

Three hypotheses that do not scope a skill

Hypothesis 1: the skill's allowed-tools field limits it

It does not. The Claude Code skills documentation says allowed-tools "does not restrict which tools are available: every tool remains callable." It pre-approves the listed tools for the turn that invokes the skill. The same page warns that "a skill can grant itself broad tool access" and that workspace trust does not gate the field. The restricting field is disallowed-tools, which removes tools while the skill is active.

Hypothesis 2: OAuth scopes on the MCP server cover it

They cover one leg. The Claude Code MCP documentation supports oauth.scopes to pin a remote server to an approved subset of scopes. That limits what the server can do at the SaaS provider. It does nothing about a local stdio server reading ~/.aws/credentials from disk.

Hypothesis 3: the approval prompt is the boundary

The CSA post calls this the "consent gap": "Users approve what they see, but hidden subprocesses operate under the same trust context." Approval decides whether something starts. It does not decide what it can reach once running.

Root cause: the unit of isolation is the process, not the skill

Claude Code has no per-skill credential store. Every skill in a session shares one process environment, one filesystem view and one network path. Per-skill least privilege is therefore approximated by two moves. First, shrink what the session itself holds. Second, split work that needs different credentials into separate sessions or separate server processes, each with only its own secrets.

The settings that do the shrinking are documented and enforceable today.

The fix: settings that narrow the surface

Strip credentials from sandboxed commands. The sandboxing docs show this pattern for sandbox.credentials, which blocks reads of named files and unsets named variables before each sandboxed command:

{
  "sandbox": {
    "enabled": true,
    "credentials": {
      "files": [
        { "path": "~/.aws/credentials", "mode": "deny" },
        { "path": "~/.ssh", "mode": "deny" }
      ],
      "envVars": [
        { "name": "GITHUB_TOKEN", "mode": "deny" },
        { "name": "NPM_TOKEN", "mode": "deny" }
      ]
    }
  }
}

The docs note "There is no built-in credential deny list, so only the files and variables you list are restricted." A mask mode replaces a secret with a per-session placeholder that the sandbox proxy swaps for the real value only on requests to hosts you allow. Masking requires TLS termination at the proxy and is honoured only from user, managed or --settings scope, never from a repository's settings.

Strip credentials from processes the sandbox does not cover. The same page says that to strip credentials from all subprocesses regardless of sandboxing, set CLAUDE_CODE_SUBPROCESS_ENV_SCRUB.

Give each MCP server only its own secret. Pass keys per server with claude mcp add --env KEY=value, or the env block in .mcp.json, and keep them out of the shell profile the agent launches from. For remote servers, the MCP docs say Claude Code reads named credential variables such as ANTHROPIC_API_KEY "as empty rather than expanding them" in url and headers, so a project's .mcp.json cannot ship those to a server it names. Other variable names expand as written.

Close the shell paths a skill can open. Setting "disableSkillShellExecution": true replaces ` !command injection in user, project and plugin skills with a placeholder. Deny specific skills with Skill(name) permission rules. Set "allowUnsandboxedCommands": false` so a failed command cannot be retried outside the sandbox.

Stop repositories from granting themselves tools. Under allowManagedPermissionRulesOnly in managed settings, the skills docs say Claude Code ignores allowed-tools in project and personal skills (v2.1.282 or later).

Restrict the network path. Set sandbox.network.allowedDomains and strictAllowlist so sandboxed commands reach only listed hosts. This limits what a skill's script can send even when it reads something it should not.

Portable artifact: the per-skill capability matrix

Fill one row per skill or MCP server. The "needs" columns come from reading its code and docs. The "reachable today" column comes from your actual session config. Any cell where reachable exceeds needs is the work.

Name / version | Type   | Needs secrets      | Needs shell | Needs hosts         | Reachable today            | Control to apply
---------------|--------|--------------------|-------------|---------------------|----------------------------|------------------------------
pdf-tools 1.2  | skill  | none               | python only | none                | all env, ~/.aws, any host  | credentials deny; no domains
jira-mcp 0.9   | MCP    | JIRA_TOKEN         | none        | yourco.atlassian... | all env (stdio child)      | --env JIRA_TOKEN only; scrub
deploy         | skill  | AWS role (prod)    | aws, git    | *.amazonaws.com     | all env, any host          | separate session; manual only

Rows are illustrative examples, not audit results. Mark deploy-style skills disable-model-invocation: true so only a person can start them.

Trade-offs and residual risk

  • Breakage. Denying ~/.aws/credentials breaks any skill that legitimately calls AWS from the shell. That is the point, but expect support tickets in the first week.
  • MCP servers stay outside. The sandbox does not wrap local MCP servers or hooks. For those, the documented answer is to run Claude Code itself inside a container, VM or the open-source @anthropic-ai/sandbox-runtime.
  • Allowlists leak. The sandbox docs warn that broad domains such as github.com "can create paths for data exfiltration," because the proxy decides on the client-supplied hostname without inspecting TLS.
  • Other agents differ. Cursor, Codex and OpenClaw expose different controls. The matrix carries over; the setting names do not.

Reproduction notes

Verify the sandbox boundary the way the docs suggest: ask the agent, not your own ! prompt, to run curl --noproxy '*' https://example.com. Inside the sandbox it should fail with Could not resolve host. Then ask it to run env | grep -c TOKEN before and after adding credentials.envVars deny entries. A count that does not drop means the setting is not loading from the scope you edited. Put these rules in user or managed settings: the docs say a deny entry only narrows access, and no other scope can remove it.

A known-bad list catches skills already reported; scoping limits the damage from the ones nobody has reported yet. The threat.cafe board is a place to check the first while you set up the second.

Sources

#least-privilege #agent-skills #mcp #claude-code #sandbox

All posts