How Claude Code runs tools safely, isolates execution, and manages permissions
Claude Code is an environment where Claude can interact with your system—reading files, running commands, calling APIs, and more. But Claude runs in the cloud. Your system is local. How does Claude actually execute code on your machine safely?
The answer is the Claude Code Harness: a runtime manager that sits between Claude's decisions and your system's resources. Think of it as a careful gatekeeper.
Before the harness existed, there would be three options: either Claude could do anything (dangerous), Claude could do nothing (useless), or someone would manually approve each command (tedious). The harness automates the "who decides what's allowed" question using a configuration layer.
Imagine you're hiring a contractor to fix your house. You wouldn't give them unlimited access—you'd say "you can use the kitchen, the hallway, and the bathroom; you cannot touch the bedroom." That set of rules is a harness.
The contractor (Claude) decides what work to do (fix the sink). The harness (your rules) decides whether that work is allowed (yes, if the sink is in the kitchen). If the contractor tries to paint the bedroom (not allowed), the harness stops it.
Here's a concrete example:
// Claude decides: "I'll read the config file"
const content = await bash('cat /src/config.json');
// Harness checks: "Is /src readable?"
// Harness config says: "Yes, /src is readable"
// Result: Harness allows it, returns file content
// Claude decides: "I'll delete the system folder"
const result = await bash('rm -rf /etc');
// Harness checks: "Is /etc writable?"
// Harness config says: "No, /etc is not writable"
// Result: Harness blocks it, returns Permission Denied
This is the core insight: Claude proposes actions, the harness enforces policy.
The harness manages three kinds of operations:
| Tool Type | Example | What Harness Controls |
|---|---|---|
| Bash | bash('ls -la /home') |
Which directories are readable/writable, which commands are allowed |
| MCP Servers | invokeToolViaAgent('github') |
Which MCP servers are reachable, which tool parameters are allowed |
| File I/O | read('/src/file.txt') |
Which paths can be read, which can be written, file size limits |
All three flow through the same harness enforcement: request → permission check → execute → collect output → filter → return result.
This is the most important concept. The harness and Claude have different jobs.
Example scenario: Claude wants to install a package.
Claude's decision: "This project needs the 'express' library. I'll run npm install express"
Harness's decision: "Can npm run? Can it write to node_modules? Is the internet available? Timeout is 60s."
Two possibilities:
1. Config allows npm, node_modules is writable:
→ Harness: "Allowed. Here's the result."
→ Claude: "Great, now I can use express"
2. Config disallows npm:
→ Harness: "Permission denied: npm is not in allowlist"
→ Claude: "Oh, I can't run npm here. Let me try a different approach"
{success: false, error: "PermissionError", message: "npm not in allowlist"}. Claude can then make a different decision.
This division of responsibility is why Claude Code can be both powerful and safe. Claude has the intelligence to solve problems. The harness has the policy to prevent harm.
The harness doesn't reinvent authorization. It uses a simple allowlist + blocklist model for each kind of resource.
When Claude makes a request, the harness runs a series of checks in order. First match wins.
REQUEST: bash('cat /src/readme.md')
CHECKS:
1. Is 'bash' in the tool allowlist? → YES
2. Is '/src' readable? → YES
3. Any environment restrictions? → NO
4. Timeout OK (120s)? → YES
RESULT: Permission granted. Execute command.
If any check fails, the request is denied:
REQUEST: bash('docker run ubuntu')
CHECKS:
1. Is 'bash' in the tool allowlist? → YES
2. Is '/src' readable? → N/A (not accessing files)
3. Any environment restrictions? → NO
4. Timeout OK (120s)? → YES
RESULT: Permission granted. Execute bash.
BUT THEN:
SUBPROCESS CHECK: bash tries to run 'docker'
1. Is 'docker' in the tool allowlist? → NO
RESULT: Permission denied. Bash returns error.
bash('rm -rf /etc') and the harness has blocked /etc writes, the filesystem itself will deny it—double protection.
Here's where the harness earns its keep: when something fails, only that operation fails. The harness keeps running.
Without a harness, if Claude ran a bad command, the whole session could crash. With the harness, Claude gets a structured error and can decide what to do next.
SCENARIO 1: Permission Denied
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude: "Read /root/secret.txt"
Harness: "Permission denied: /root not readable"
Claude: "OK, I'll read /home instead"
Harness: "OK, here's /home/file.txt"
→ SESSION CONTINUES
SCENARIO 2: Tool Timeout
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude: "Run npm install (large project)"
Harness: [executing...]
After 120s: "Timeout: npm did not complete in time"
Claude: "I'll try with --legacy-peer-deps"
Harness: "OK, retrying"
→ SESSION CONTINUES
SCENARIO 3: Command Fails
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude: "Build the project"
Bash: "Build failed: syntax error in src/main.js"
Harness: "Tool failed with exit code 1. Here's stderr."
Claude: "Found the error. Let me fix it."
→ SESSION CONTINUES
The key is that errors are data, not crashes. The harness returns structured errors:
{
success: false,
error: "PermissionError",
message: "File not readable: /etc/passwd",
toolName: "bash",
exitCode: 1,
stderr: "bash: /etc/passwd: Permission denied",
timestamp: "2026-08-17T14:30:00Z"
}
Claude sees this structured response and adapts. This is resilience by design.
The harness is configured via your project's .claude/settings.json or .claude/settings.local.json. You control what's allowed.
The golden rule: grant the minimum permissions Claude needs to do the job. Not more, not less.
// .claude/settings.json
{
"permissions": {
"bash": ["enabled"],
"allowedTools": ["bash", "npm"],
"fileAccess": {
"read": ["/src", "/docs"],
"write": ["/tmp", "/src/dist"],
"deny": ["/etc", "/root", "/home"]
},
"network": {
"allow": ["api.github.com", "registry.npmjs.org"],
"deny": ["*"]
},
"timeouts": {
"bash": 120,
"mcp": 30,
"fileIo": 10
}
}
}
What this does:
bash and npm, nothing else/src and /docs/tmp and /src/dist only/etc, /root, or /homeThis is normal and useful. Claude will see an error like:
Error: Permission denied: docker not in allowlist
Claude's next thought: "I can't run docker. Let me try building
with npm instead, or ask the user to enable docker if they want
container builds."
This is the harness working as intended. Constraints are features, not bugs.
These questions check whether you understand the core concepts. Read carefully—each question has a correct answer and a reason why.