---
name: set-posture
description: Translate `.ravenclaude/comfort-posture.yaml` into `.claude/settings.json` permission rules. Owns the (category, level) → permission-rule emission table that the `/set-posture` slash command applies. Read this skill when authoring or reviewing the translation logic, when a category is missing rules, or when a level's behavior surprises you.
---

# Skill: set-posture

This skill is the canonical reference for **how comfort-posture YAML maps to Claude Code permission rules**. The slash command `/set-posture` invokes a Python script that implements this mapping; this file documents the design and is the place to extend it (new categories, new buckets, time-boxed elevations, MCP per-server trust).

## The translation pipeline

```mermaid
flowchart LR
  DASH[Dashboard Settings tab] -->|Save to repo| YAML[.ravenclaude/comfort-posture.yaml]
  YAML -->|/set-posture| TR[apply-comfort-posture.py]
  TR -->|overwrite| RULES[.claude/settings.json allow/ask/deny]
  RULES --> ENGINE[Claude Code engine]
  LOCAL[.claude/settings.local.json] -->|merged on top| ENGINE
```

The skill owns the translation step. The dashboard owns the input. The script owns the file I/O. v0.17.0 switched from snapshot-merge to clean overwrite: the posture YAML is the single source of truth for `permissions.{allow,ask,deny}`. Personal overrides belong in `.claude/settings.local.json`, which Claude Code merges on top.

## Level → bucket (v0.19.0)

The vocabulary is three levels, one per bucket:

| YAML level | settings.json bucket |
|---|---|
| `deny` | `permissions.deny` |
| `ask` | `permissions.ask` |
| `allow` | `permissions.allow` |

Earlier releases exposed five levels (`always-ask`, `mostly-ask`, `mostly-allow`, `autopilot` alongside `deny`) that already collapsed to these same three buckets — `always-ask` ≡ `mostly-ask` and `mostly-allow` ≡ `autopilot`. The planned v0.2.0 split that would have made those pairs behave differently (a per-category `safe_patterns` / `risky_patterns` shape) was never shipped, so the extra levels were always cosmetic. v0.19.0 retires them from the dashboard. The translator still **accepts** the old names so a consumer's pre-0.19.0 `comfort-posture.yaml` keeps translating unchanged after a `/plugin marketplace update`; see `level_to_bucket()` in `apply-comfort-posture.py`.

## Category → patterns (v0.1.0)

Each category emits a flat list of narrow Bash patterns plus the relevant file/network/MCP shapes. The full list lives in `apply-comfort-posture.py` `EMISSIONS` constant. The patterns chosen for v0.1.0 are reproduced below for review.

### File categories

| Category | Patterns |
|---|---|
| `file_read_project` | `Read(**)` |
| `file_edit_project` | `Edit(**)`, `Write(**)`, `MultiEdit(**)` |
| `file_read_global` | `Read(~/**)`, `Read(//**)` † |
| `file_edit_global` | `Edit(~/**)`, `Write(~/**)`, `Edit(//**)` †, `Write(//**)` † |

The path anchors follow Claude Code's documented gitignore-style anchors (see `knowledge/claude-code-permissions.md` §"Read/Edit path anchors"). `/path` anchors at project root, `~/path` at home, `//abs/path` at filesystem root, bare paths default to cwd.

**† Filesystem-root catch-alls are suppressed in the `ask` bucket.** `//**` is anchored at the filesystem root, so it also matches *project-internal* paths. Because Claude Code resolves overlaps as deny > ask > allow, an `ask`-bucket `Read(//**)` would beat the project category's `Read(**)` allow rule and prompt on **every in-project read** (and likewise for edits) — silently defeating `file_read_project: allow`. So when `file_read_global` / `file_edit_global` resolve to the `ask` level, the generator drops the `//**` patterns and lets Claude Code's built-in "ask on any unmatched path" handle genuinely-external reads. The `//**` patterns are still emitted for `allow` levels (where you *want* to auto-approve the whole tree) and `deny` levels. The home anchor `~/**` is kept in all buckets — it doesn't overlap the project. Implemented as `FS_ROOT_CATCHALLS` in `apply-comfort-posture.py`'s `compute_emission()`.

### Shell categories

Each shell category is a curated list of narrow command-prefix patterns. Narrow is essential — broad patterns like `Bash(*)` get silently dropped by auto-mode. See `apply-comfort-posture.py` for the exhaustive lists.

Highlights:
- `shell_readonly` — 30+ patterns covering ls/cat/head/tail/grep/find/git-read/gh-read
- `shell_local_mutate` — mkdir/touch/cp/mv/rm/chmod plus git-local (commit, checkout, stash, restore, reset, merge, rebase)
- `shell_remote_mutate` — git push/fetch/pull + gh pr/issue mutations + npm publish
- `shell_code_exec` — interpreter commands (python, python3, node, deno, bash -c, sh -c, eval, ruby, perl)
- `shell_package_install` — npm/pnpm/yarn/pip/uv/brew/apt/cargo/go install variants

### Network categories

| Category | Patterns |
|---|---|
| `network_read` | `WebFetch`, `Bash(curl:*)`, `Bash(wget:*)` |
| `network_write` | `Bash(curl -X POST:*)`, `Bash(curl -X PUT:*)`, `Bash(curl -X DELETE:*)`, `Bash(curl -X PATCH:*)`, `Bash(gh api PATCH:*)`, `Bash(gh api POST:*)`, `Bash(gh api DELETE:*)`, `Bash(gh api PUT:*)` |

### MCP tools (deferred)

`mcp_tools` is intentionally empty in v0.1.0's `EMISSIONS`. Per-MCP-server trust is configured in Claude Code's user settings (`~/.claude/settings.json`); comfort-posture's `mcp_tools` is a global default that doesn't have a direct one-to-one rule mapping. v0.2.0 should map this to per-server rules once the marketplace has a stable list of MCP servers consumers connect.

## Overwrite semantics (v0.17.0+)

The script **overwrites** `permissions.allow`, `permissions.ask`, and `permissions.deny` in `.claude/settings.json` with the resolved emission. Non-posture fields (`$schema`, `model`, `env`, `hooks`, `permissions.additionalDirectories`) are untouched.

Why overwrite instead of merge: v0.16.0 used a snapshot-based merge that preserved hand-added rules. In practice that produced **bucket collisions** — the same pattern landing in both `allow` (hand-added) and `ask` (posture). Per Claude Code's precedence (deny > ask > allow), the rule effectively becomes `ask`, silently downgrading the user's intended workflow. Overwrite removes that footgun: the YAML is authoritative; you see what you set.

Implications:
- **Personal overrides go in `.claude/settings.local.json`.** Claude Code merges that file on top of `settings.json`, so anything you put there survives every `/set-posture` run.
- **Hand-edits to `settings.json`'s `permissions.{allow,ask,deny}` are wiped.** If you find yourself wanting to add a rule there, ask whether it belongs in the posture YAML (so the dashboard reflects it) or in `settings.local.json` (personal-only).
- **Stale snapshot files are cleaned up.** If a v0.16.0 `.claude/_comfort-posture-snapshot.json` is present, the script deletes it on first v0.17.0 run.

## Schema v5 — per-layer authoring (v0.18.0+)

v5 lets each category carry **separate levels for the three settings layers** Claude Code merges at runtime. The dashboard's expandable per-layer cards author this; `apply-comfort-posture.py`'s `run_v5()` emits one settings file per active layer.

```yaml
schema_version: 5
security_deny: [ ... ] # floor (unchanged)
categories:
  shell_local_mutate:
    user: allow # one of: allow | ask | deny | inherit
    local: ask
    project: inherit # inherit = emit nothing at this layer
```

| Layer | Settings file | Audience |
|---|---|---|
| `user` | `~/.claude/settings.json` | this machine, all projects (ephemeral in a Codespace) |
| `local` | `.claude/settings.local.json` | this project, just me (gitignored; auto-added to `.gitignore`) |
| `project` | `.claude/settings.json` | the whole team (committed); **always carries the `security_deny` floor** |

Resolution: **deny > ask > allow** across the merged set — the strictest layer wins, so a personal `allow` cannot loosen a team `ask`/`deny` (this is also why putting personal preferences at `project` scope is a footgun the dashboard warns about). The per-layer value vocabulary is the 4-value `allow | ask | deny | inherit`; the `//**`-in-ask suppression (above) applies per layer.

Mechanics:

- **`--scope {user,local,project,all}`** (default `all`) chooses which layer(s) to write; the dashboard's "Save & apply all layers" uses `all`.
- **`--preview-merge`** reads the three files, computes the merged effective posture, and prints it (deny/ask/allow) — the CLI counterpart to the dashboard's effective badges.
- **Per-scope side-cars** (`.claude/.comfort-posture-applied.{local,project}.json`, `~/.claude/.comfort-posture-applied.user.json`) record what we wrote, so emptying a layer in the YAML clears that layer's posture rules on the next apply instead of orphaning them.
- The `security_deny` floor is emitted into the **project** layer only — deny is absolute under the merge, so one copy protects every layer and keeps the shared safety baseline in the shared file.
- **Back-compat:** v3/v4 single-layer postures still run the original single-file `compute_emission` path (project layer only). Only `schema_version: 5` triggers `run_v5()`.
- **Migration note:** there is intentionally NO blocking migration banner — it would halt the dashboard's auto-apply flow. The dashboard handles v4→v5 migration on load (existing levels land on the Local layer).

## Always-on security deny

`security_deny:` is a top-level list in the posture YAML carrying patterns that are **always denied regardless of category levels**. The default list covers `.env` / `.pem` / `credentials*` reads and the most common destructive shell commands (`rm -rf`, `git push --force`, `git reset --hard`, `curl | sh`, `sudo`). Under schema v5 it is emitted into the **project** layer (the committed, always-present floor).

- Add patterns to tighten your security floor.
- Remove patterns only if you have a specific reason — these are the marketplace's recommended security baseline.
- The patterns survive every preset (Recommended, Deny, Ask, Allow) because they live outside `categories:`.

## Hook trust flags (read directly by hooks, NOT translated to settings.json)

Two top-level posture keys carry **trust flags** that hooks read at runtime — they do not become Claude Code permission rules. They control whether the corresponding hook surfaces a first-run / first-use confirmation prompt, or skips straight to its trusting behavior. Both default to `false` (confirm) so an untrusted repo can't auto-execute or auto-allow on the first touch.

| Key | Read by | When `false` (default) | When `true` |
|---|---|---|---|
| `definition_of_done.trusted` | `dod-gate.sh` | First run per session per cmd-hash: deny + stderr `touch <confirm-path>` to authorize | Skip first-run confirm; `bash -c $cmd` runs as before |
| `web_access.trusted` | `guard-web-access.sh` | First WebFetch per domain per session to a YAML-whitelisted host: `permissionDecision: ask` | Skip first-use ask; whitelisted hosts auto-allow as before |

These flags address Codex desktop trust review Findings 1 + 5 (an untrusted YAML edit auto-executing on `Stop` / auto-allowing on `WebFetch`). They are tool-layer trust signals — `security_deny` is still the layer-independent floor.

**Authoring:** in the dashboard, both flags will surface as a single "Trust this repo's YAML for…" toggle per trust surface (UI TBD). For now, hand-edit `comfort-posture.yaml` or copy the documented examples from `templates/comfort-posture-balanced.yaml`.

## Per-pattern overrides (v0.17.0+)

Each `categories.<name>` value can be either a plain string (level applied to every pattern in the category) or an object with per-pattern overrides:

```yaml
categories:
  shell_readonly: allow             # all patterns: allow
  shell_local_mutate:               # mixed
    default: allow
    overrides:
      "Bash(rm:*)": ask
      "Bash(git reset:*)": ask
```

Resolution precedence (highest wins): per-pattern override > category default > top-level `global_default`. Use overrides to tighten or relax specific patterns within a category without splitting the category itself.

### Per-permission, per-layer overrides (v0.19.0)

Under schema v5 the `overrides:` map keys a permission to a **per-layer object**, so a single permission can be set independently at User / Local / Project — the same `allow | ask | deny | inherit` vocabulary as the category layers, where `inherit` defers to the category-wide layer value:

```yaml
categories:
  shell_remote_mutate:
    user: inherit
    local: ask # the category asks before any remote mutation at my layer …
    project: inherit
    overrides:
      "Bash(git push:*)":
        user: inherit
        local: deny # … but git push is hard-blocked just for me
        project: inherit
```

The dashboard authors this via the per-permission controls inside each category card. Resolution at each layer: per-permission override (non-`inherit`) wins over the category-wide layer value; layers then merge `deny > ask > allow` as usual. See `category_overrides_map()` / `pattern_layer_value()` in `apply-comfort-posture.py`.

The pattern strings must match exactly what `EMISSIONS` in `apply-comfort-posture.py` declares — copy them from the dashboard's per-category list rather than typing freehand.

## Session-mode interactions

Critical context to communicate to users in every run. The script prints this footer:

> Note: comfort-posture works best with session mode at 'default'.
>   - Plan mode and Accept-edits compose fine.
>   - Auto-mode silently drops broad allow rules by design. The rules
>     this script emits are narrow, so most categories survive auto-mode —
>     but expect shell_code_exec and shell_package_install to be partially
>     overridden when auto-mode is on.
>   - bypassPermissions bypasses these rules entirely (use sparingly).

See `knowledge/claude-code-permissions.md` "Permission modes" for the full table.

## When to extend this skill

- **New category** in `dashboard-schema.json` → add an entry to `EMISSIONS` in the Python script AND a row in this skill's category-patterns section.
- **New level** in the YAML schema → update `level_to_bucket()` in the Python script.
- **Time-boxed elevations** (proposal 002 §6.2) → add an `elevations:` block parser to the Python script; remember to expire.
- **Per-MCP-server trust** → add a new section to `EMISSIONS` and to the dashboard schema; emit `mcp__<server>__*` rules.

Each extension should:
1. Update the Python script's `EMISSIONS` table.
2. Update this skill's tables to match.
3. Add a row to the dashboard's category list if user-visible.
4. Test by hand (`--dry-run` on a sample YAML).

## See also

- `commands/set-posture.md` — the slash-command entry that invokes the script.
- `scripts/apply-comfort-posture.py` — the implementation.
- `dashboard-schema.json` — the YAML shape this consumes.
- `knowledge/claude-code-permissions.md` — the permission model these rules feed into.
- `docs/proposals/2026-05-22-002-comfort-posture-mechanism.md` — the design proposal that motivated this skill.
