The junior pastes a settings.json screenshot. sandbox.excludedCommands has docker *. The coding agent ran npm ci && docker build .. The first token looks like a match. They file a sandbox hole: “the whole line ran outside the box.”
I stop the run there. One matching glob is not a sandbox exemption for the rest of the line. Official settings docs are blunt: your entries take a Bash call out of the sandbox only when they cover every command in it. A docker * entry alone does not take npm ci && docker build . out of the sandbox. [Source: https://code.claude.com/docs/en/settings-reference]
I already refused to treat a failed local DNS lookup as a missing host in Handing the Hostname to the Forward Proxy Is Not a Missing Host. I already refused to treat a present AGENTS.md as the project instructions while CLAUDE.md exists in A Present AGENTS.md Is Not the Project Instructions While CLAUDE.md Exists. I already refused to treat ubuntu-latest as a runner image I already tested in Ubuntu-latest Is Not a Runner Image You Already Tested. This post is the same desk rule for compound Bash. Print the glob. Split the parts. Name who owns sandbox.excludedCommands.
The question is not whether docker * demos on a single command. The question is whether the named owner can prove every part of the line matched.

The ticket that looks like a hole
Juniors treat excludedCommands the way they treat a shell alias. If the first token matches, the whole line is “allowed out.” That habit is correct for a personal alias. It is wrong for Claude Code’s sandbox.
Two jobs collide on that line.
- Run the tool that cannot live in the box. Docker, a host builder, a binary that needs a device the sandbox does not give.
- Keep the rest of the line in the box. Package install, file writes, network, anything that was not named.
The GitHub release that named this field says the product used to exempt an entire compound Bash command from the sandbox when only one part matched. Every part must now match. The official changelog repeats that sentence under 18 September 2026. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.277] [Source: https://code.claude.com/docs/en/changelog]
If you only screenshot docker * and the first token, you will file a hole. You will not file the line.
I do not invent a fake overnight breach. I use the public contract. The settings page is the ticket, not a version pin in the title.
What excludedCommands actually grants
Official docs: sandbox.excludedCommands names commands Claude Code runs outside the sandbox, such as tools that do not work under it. Each entry uses the same syntax as the content of a Bash(...) permission rule: an exact command, a prefix such as docker *, or a wildcard pattern. [Source: https://code.claude.com/docs/en/settings-reference]
Read the next sentence twice. Your entries take a Bash call out of the sandbox only when they cover every command in it, and some call shapes stay sandboxed even then.
That is the field. Exclusion is a convenience, not a security boundary. Official docs say prefer filesystem.allowWrite when a tool only needs to write somewhere specific. Excluded commands still go through the regular permission flow. Claude Code merges entries across every settings scope the session loads, and there is no managed-only lock for this list, so keep a managed list narrow. [Source: https://code.claude.com/docs/en/settings-reference]
The example the docs publish is this shape. Copy it as a probe, not as a bypass recipe.
1{
2 "sandbox": {
3 "excludedCommands": ["docker *"]
4 }
5}
[Source: https://code.claude.com/docs/en/settings-reference]
docker * is not “anything that mentions docker.” It is one pattern. npm ci is a different command. A chain is more than one command.
Pins are evidence, not the hook
sandbox.excludedCommands. This morning’s npm registry, 23 September 2026: @anthropic-ai/claude-code latest and next are 2.1.280 (published 22 September 2026). stable is still 2.1.267 (published 9 September 2026). Do not treat stable as the field. Do not put those numbers in the title. Changelog notes after 18 September cover other tickets. This post is the compound line. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.277] [Source: https://code.claude.com/docs/en/changelog] [Source: https://www.npmjs.com/package/@anthropic-ai/claude-code]Every part of the line has to match
I keep one table on the ticket.
| Line the agent ran | excludedCommands | Field |
|---|---|---|
docker build . | docker * | Pattern covers the only command. Exclusion can fire. Permission flow still runs. |
npm ci && docker build . | docker * | npm ci is not covered. The call stays sandboxed. |
cd build && docker compose up | docker * plus a cd entry | Official docs: still sandboxed. A cd anywhere in the call keeps the whole call in the box. |
[Source: https://code.claude.com/docs/en/settings-reference]
The middle row is the junior screenshot. They listed docker. They saw &&. They assumed the shell inherited the exemption. The product no longer does that. The GitHub body and the changelog both say the old glob exempted the entire compound command when only one part matched. That is the behavior you must not file as current. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.277] [Source: https://code.claude.com/docs/en/changelog]
The last row is the trap after you “fix” the middle row. You add cd to the list because the agent cds into build/. Official docs still keep that call sandboxed. Adding a cd entry does not change it. [Source: https://code.claude.com/docs/en/settings-reference]

If you need write access for a build artifact, official docs point at filesystem.allowWrite, not at a wider glob. [Source: https://code.claude.com/docs/en/settings-reference]
Shapes that stay sandboxed anyway
Even when every named part looks like a match, official docs keep a Bash call sandboxed for these shapes, among others:
- A command starting with
sudo,eval, orxargs - A
cd,pushd, orpopd, wherever it appears in the call - A command substitution, a subshell, or a control-flow block such as
iforfor - A redirection, such as
docker build . > build.log, other than one that only duplicates a file descriptor, as2>&1does - A command name that comes from a variable
[Source: https://code.claude.com/docs/en/settings-reference]
That list is why I refuse “we listed docker, so compose is out.” docker build . > build.log stays in. cd build && docker compose up stays in. A variable that expands to docker stays in.
Sandboxing docs also say content-scoped ask rules such as Bash(git push *) still force a prompt even for sandboxed commands. A bare Bash ask rule, or Bash(*), is skipped for commands that run sandboxed, and still applies when the command falls back to the regular permission flow. Exclusion does not delete that flow. [Source: https://code.claude.com/docs/en/sandboxing]
Strict sandbox mode is a different switch. sandbox.allowUnsandboxedCommands: false makes Claude Code ignore dangerouslyDisableSandbox. Every command Claude runs must run sandboxed unless you listed it in excludedCommands. The /sandbox Overrides tab shows that as Strict sandbox mode. [Source: https://code.claude.com/docs/en/settings-reference] [Source: https://code.claude.com/docs/en/sandboxing]
Do not mix those tickets. Strict mode is “no escape hatch.” Compound glob is “one match is not the whole line.” File one.
Probe the glob, not the feeling
I do not reconstruct a compound line in a live agent session to “prove” a hole. That is how you write an escape recipe. I print the list, split the line, require a human owner, and fail closed when the owner is unset.
1#!/usr/bin/env python3
2"""Probe sandbox.excludedCommands coverage. Does not run the agent. Does not spawn a shell."""
3
4from __future__ import annotations
5
6import json
7import os
8import re
9from pathlib import Path
10
11OWNER_ENV = "SANDBOX_EXCLUSION_OWNER"
12SPLIT = re.compile(r"\s*(?:&&|\|\||;|\|)\s*")
13
14
15def load_patterns(settings_path: Path) -> list[str]:
16 data = json.loads(settings_path.read_text(encoding="utf-8"))
17 sandbox = data.get("sandbox") or {}
18 patterns = sandbox.get("excludedCommands") or []
19 if not isinstance(patterns, list):
20 raise SystemExit("FAIL=sandbox.excludedCommands is not a list")
21 return [str(p) for p in patterns]
22
23
24def split_parts(command: str) -> list[str]:
25 return [part.strip() for part in SPLIT.split(command) if part.strip()]
26
27
28def glob_to_re(pattern: str) -> re.Pattern[str]:
29 return re.compile("^" + re.escape(pattern).replace(r"\*", ".*") + "$")
30
31
32def covered(part: str, patterns: list[str]) -> bool:
33 return any(glob_to_re(p).match(part) for p in patterns)
34
35
36def main() -> int:
37 owner = os.environ.get(OWNER_ENV, "UNSET")
38 settings = Path(os.environ.get("CLAUDE_SETTINGS", ".claude/settings.json"))
39 command = os.environ.get("AGENT_BASH_LINE", "")
40 print(f"OWNER={owner}")
41 print(f"SETTINGS={settings}")
42 print(f"LINE={command}")
43 if owner == "UNSET":
44 print("FAIL=name SANDBOX_EXCLUSION_OWNER before you file the agent")
45 return 1
46 if not settings.is_file():
47 print("FAIL=settings file missing")
48 return 1
49 if not command:
50 print("FAIL=set AGENT_BASH_LINE to the exact command from the transcript")
51 return 1
52 patterns = load_patterns(settings)
53 print("PATTERNS=" + json.dumps(patterns))
54 parts = split_parts(command)
55 print("PARTS=" + json.dumps(parts))
56 hits = [covered(part, patterns) for part in parts]
57 for part, hit in zip(parts, hits):
58 print(f"PART_COVERED={hit} {part}")
59 if len(parts) > 1 and not all(hits):
60 print("VERDICT=COMPOUND_NOT_FULLY_COVERED")
61 return 2
62 if all(hits) and parts:
63 print("VERDICT=EVERY_PART_MATCHED_PATTERN")
64 print("NOTE=docs still keep sudo/eval/xargs/cd/redirection/subshell shapes sandboxed")
65 return 0
66 print("VERDICT=NO_PARTS")
67 return 2
68
69
70if __name__ == "__main__":
71 raise SystemExit(main())
Run it in the app root the coding agent uses, not in a docs folder on your laptop.
1export SANDBOX_EXCLUSION_OWNER="shinjae"
2export CLAUDE_SETTINGS=".claude/settings.json"
3export AGENT_BASH_LINE='npm ci && docker build .'
4python3 scripts/probe_excluded_commands.py
5claude --version
The script does not talk to Anthropic. It does not start Docker. VERDICT=COMPOUND_NOT_FULLY_COVERED means you do not have a whole-line exemption. Then open /sandbox. Copy Excluded commands. Copy the transcript line. Put those four lines on the ticket: owner, patterns, parts, verdict.
A second probe is the session itself. Type /sandbox. Read the Overrides tab. If Strict sandbox mode is on, dangerouslyDisableSandbox is not an extra hatch. That is a different screenshot from the glob list. [Source: https://code.claude.com/docs/en/sandboxing]

What you must not do
Forbidden:
- File a “sandbox hole” ticket without printing
excludedCommands, the split parts,/sandboxExcluded commands, and one human name on the list. - Put a Claude Code version in the title or the first line. The pin is evidence after the decision.
- Mix this field with a hostname-to-proxy DNS ticket, an
AGENTS.mdload ticket, or anubuntu-latestrunner label. Those are other posts. - Treat
stableon npm as the field. This morningstableis still behind the compound-glob fix. [Source: https://www.npmjs.com/package/@anthropic-ai/claude-code] - Reconstruct
cmd && otherin a live session to demonstrate an old whole-line exemption. That is an escape write-up. Print and split. Do not chain. - Add
cdtoexcludedCommandsand claimcd build && docker compose upis now out. Official docs keep that call sandboxed. [Source: https://code.claude.com/docs/en/settings-reference] - Widen the glob until
*covers the rest of the line, then call that a security policy. Exclusion is a convenience. Preferfilesystem.allowWritefor a specific path. [Source: https://code.claude.com/docs/en/settings-reference] - Recommend buying a plan, a model, or a seat because a docker glob did not unsandbox
npm ci. - Treat a permission allow rule as isolation. Allow answers “may it run.” Exclusion answers “does it leave the box.”
Allowed:
- Print
sandbox.excludedCommandsfrom every settings scope the session loads. - Split the transcript line on
&&,;,||, and|. - Open
/sandboxand screenshot Excluded commands plus Strict sandbox mode. - Keep
filesystem.allowWritefor the artifact directory instead of a wider glob. - Name one human as
SANDBOX_EXCLUSION_OWNER. - Print
claude --versionafter the decision, in a details block, not in the title.
GSC this week still has no striking-distance query on the hostname-to-proxy post, the AGENTS.md post, or the ubuntu-latest post. I am not refreshing those URLs. This is a new field, not a synonym of Monday’s proxy env or Sunday’s instruction file.
If you need the broader habit, start at /ai-agent-operations/. Tooling notes live under /developer-tools/. Laravel plus Vue notes live under /laravel-vue-saas/ when the agent is touching that stack. A first-week map is at /start-here/.
A changelog bullet about a glob is not permission to skip the split.

What you should do Monday morning
- Open the repo that actually ships. Export
SANDBOX_EXCLUSION_OWNERto a human name. Runprobe_excluded_commands.pyagainst the exactAGENT_BASH_LINEfrom last week’s transcript. Write the verdict on the ticket next to that name. - Open Claude Code in that same directory. Type
/sandbox. Copy Excluded commands. Copy whether Strict sandbox mode is on. If the panel has no such row, you are not looking at the session that ran the line. - If the list contains
docker *and the transcript containsnpm ci && docker build ., do not file a hole. File “npm ci was never covered.” Decide whether npm belongs in the box withfilesystem.allowWrite, or whether the agent must run docker as its own command. - If the transcript contains
cdanywhere, treat the whole call as sandboxed until a human proves otherwise. Do not add acdentry and stop. - Print
claude --version. Ifstableis what the laptop installed, do not treat that pin as the compound-glob field. Compare it to the GitHub release that said every part must match, then decide an upgrade as change control, not as a social post. - Confirm coding-agent instructions on this desk name the same owner and forbid “the glob unsandboxed the line” without the split. A prompt that says “docker is excluded” while the agent chains npm is a different ticket.
The question is not whether docker * demos on an empty line. The question is whether the exemption survives maintenance, handoff, and a junior who already chained two commands.
Further reading
Source Claude Code Docs — settings reference (sandbox.excludedCommands)
Source Claude Code Docs — sandboxing
Source GitHub — Claude Code release that required every compound part to match
