# Claude Code v2.1.216

> Claude Code v2.1.216, released 20 Jul 2026 (2026-07-20). 15 entries read out of the shipped bundle. Unofficial, and not affiliated with Anthropic.

Web version: https://changelogs.core-directive.com/v/2.1.216

Version 2.1.216 introduces the `/workshop` skill for running interactive decision workshops through published artifacts, adds JWT-aware credential masking for sandbox environments, and hardens the `/auto-mode-setup` command with mandatory SHA-256 file hashing. A large internal refactor upgrades MCP tool schema validation to JSON Schema draft 2020-12 and tightens worktree and file-history safety checks.

## New Features

### Workshop Artifact Skill

**What**

A new built-in `/workshop` skill that runs iterative decision workshops through published artifacts — Claude presents choices as decision blocks, readers click their pick on the live page, and Claude applies each choice and republishes until the workshop is finalized.

**Usage**

`/workshop` Then follow the skill's instructions to create a `.workshop.md` file.

**Details**

- Create a Markdown file ending in `.workshop.md`; the suffix routes it through the workshop renderer
- Decision points are fenced code blocks with the `decision` info string, containing `id`, `question`, `option` rows, and an optional `lean` (recommended choice with a brief reason)
- A canonical `finalize` block ends the loop; Claude applies all pending decisions and marks the artifact complete
- Claude holds an artifact live-update subscription to receive clicks in real time; the loop is offline-first (durable store is authoritative, live notification is acceleration)
- Options and labels render as clickable spans on the published page; resolved decisions are shown with the chosen option highlighted
- Up to 20 decision blocks per document; blocks past the cap remain visible as plain fences
- Supports `anchor` field per block (e.g., a commit hash) for staleness detection

**Evidence**

Workshop skill system prompt (search for `".workshop.md"` or `"decision blocks"`). Decision block parser (search for `"uyy = \"decision\""` or `"ws-decision-"`).

### JWT Credential Masking for Sandbox Environments

**What**

The `sandbox.credentials.envVars` config now supports `decode: "jwt"` to handle JWT tokens: the token is replaced with a structurally valid but fake JWT inside the sandbox, with real claim values masked as sentinels injected into requests.

**Details**

- Add `decode: "jwt"` to a `credentials.envVars` entry to activate JWT handling
- Optional `maskClaims: ["sub", "email"]` masks specific string claims within the JWT, replacing each with a per-claim sentinel injected at the network layer
- If the environment variable is not a valid JWT, the variable is left unprotected and a warning is logged
- If `maskClaims` names a claim that is absent or not a string, that claim is skipped and logged
- Replaces the previous `extract`-regex-only approach, which remains available for non-JWT values

**Evidence**

JWT masking handler (search for `"decode \"jwt\""` or `"maskClaims"`). Fake token generator (search for `"pnu = \"c3J0LWZha2U\""` which is the fake signature base64).

## Improvements

### `/auto-mode-setup` Now Requires File Hash Before Applying

**What**

The `--apply-file` flag now requires a preceding `--expect-sha256 <64-hex>` argument. The apply step is refused unless the proposal file's exact bytes hash to the given SHA-256, preventing a file-swap attack between review and apply.

**Usage**

`# Step 1: generate proposal (unchanged) /auto-mode-setup --wizard posture=personal scope=all depth=both --propose # Step 2: apply with hash /auto-mode-setup --expect-sha256 <64-hex> --apply-file /tmp/auto-mode-proposal.json` **Details** - `--expect-sha256` must come directly before `--apply-file` and must not use `=` syntax - Optional `--request-id <uuid>` can be placed first; the UUID is echoed back in the JSON result as `requestId`, letting a host correlate replies to requests - `--request-id` must be in canonical 8-4-4-4-12 hex-and-dash UUID format - Passing `--request-id` after `--apply-file` is an error; it must come first - The hash mismatch error is: "The proposal file's bytes do not match the reviewed digest" **Evidence** New argument parser (search for `"--expect-sha256 is required"` or `"--request-id must be a UUID"`).

### MCP Tool Schema Validation Upgraded to JSON Schema Draft 2020-12

**What**

MCP tool input schemas are now validated against JSON Schema draft 2020-12. Tools with schemas that violate Anthropic API requirements are excluded when the server's tools are loaded; a system message lists which tools were dropped.

**Details**

- Added `$dynamicAnchor` and `$dynamicRef` (replacing the deprecated `$recursiveAnchor`/`$recursiveRef`)
- Added `dependentRequired` and `dependentSchemas` (splitting the old `dependencies` keyword)
- Added `maxContains` and `minContains`
- Added complete draft 2020-12 meta-schemas for core, validation, meta-data, format-annotation, content, applicator, and unevaluated vocabularies
- `tengu_mcp_normalize_root_combinators` is a per-server-URL feature flag (configured as a list of hostnames, or `"*"` for all servers): when enabled for a server, tools whose input schema has a top-level `anyOf`, `oneOf`, or `allOf` keyword are flattened — properties are merged from all combinator branches into a single flat `properties` object, required fields are collected from every branch, and a plain-text constraint note summarising the original branches is prepended to the tool's description. Without this flag, tools with top-level combinators are skipped entirely and logged as `tool_schema_normalize_gated`.
- `tengu_mcp_drop_invalid_tool_schemas` is a per-server-URL feature flag: when enabled, tools whose input schema fails draft 2020-12 meta-validation (checked via an Ajv2020 meta-validator) or whose property keys do not match `/^[a-zA-Z0-9_.-]{1,64}$/` are excluded from the tool list and added to a `droppedTools` list that feeds the "Unavailable MCP Tools" system reminder. Without this flag, such tools are kept but a debug warning is logged that API requests including them may fail.
- A system-reminder block titled "Unavailable MCP Tools" lists each excluded tool and its rejection reason

**Evidence**

New validator module (search for `"https://json-schema.org/draft/2020-12/meta/core"` or `"$dynamicAnchor"`). Exclusion message (search for `"# Unavailable MCP Tools"`).

- Flag `tengu_mcp_normalize_root_combinators`: Not enough to say (read for one account on one subscription tier against v2.1.216; this account: no value returned, anonymous baseline: no value returned, compiled default: not a boolean we can read) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.
- Flag `tengu_mcp_drop_invalid_tool_schemas`: Not enough to say (read for one account on one subscription tier against v2.1.216; this account: no value returned, anonymous baseline: no value returned, compiled default: not a boolean we can read) These values were read against a different version of Claude Code, so treat them as the nearest reading available instead of one taken on this release.

### Credential Sentinels Now Substituted in Request Bodies

**What**

Masked credential sentinels (set up via `sandbox.credentials.envVars`) are now replaced with their real values in outgoing request bodies as well as headers. A streaming `Transform` walks the body for sentinel byte sequences and swaps them in place.

**Details**

- Only applies to methods that send a body (`POST`, `PUT`, `PATCH`, etc.)
- Skipped for requests with `Content-Encoding` headers (compressed bodies cannot be substituted safely; a warning is logged)
- If any sentinel differs in length from its real value, the `content-length` header is removed so the stream can change size
- Only sentinels where sentinel length equals real-value length preserve `content-length` intact

**Evidence**

Body substitution transform (search for `"[body-substitution]"` or `"substitution skipped — a sentinel in this body"`).

### Skill Doctor Shows 7-Day Token Attribution

**What**

The `/skill-doctor` command now shows a `7d tokens` column alongside the existing `context`, `uses`, and `last used` columns, reporting how many API tokens each loaded skill has consumed over the past 7 days of sessions on this machine.

**Details**

- Token counts are aggregated from local session transcripts (cached + cache-create + uncached + output)
- Skills with no attributed tokens still show `0` rather than being omitted
- The column header is `7d tokens`
- Context cost is still shown separately as the tokens the skill's listing takes per turn

**Evidence**

New skill-doctor display (search for `"7d tokens"` or `"attributionSkill"`). Token aggregator (search for `"NOd"` with pattern `$Od` callback).

### Forge Classification for PR/MR URLs

**What**

PR and MR URLs are now classified by forge type — `github`, `github-enterprise`, `gitlab`, or `bitbucket` — based on URL shape, not just hostname. This information is surfaced to tools and hooks that work with pull/merge request URLs.

**Details**

- GitLab detected by `/-/merge_requests/` in the path
- Bitbucket detected by `/pull-requests/` in the path
- GitHub detected by matching known GitHub hostnames
- Anything else is classified as `github-enterprise`
- Described as "a naming hint, not host trust" — an unrecognised host with a PR-shaped path classifies as `github-enterprise`

**Evidence**

Forge classifier (search for `"/-/merge_requests/"` or `"forge classification derived from"`).

### Worktree Safety: Full Path-Component Symlink Checks

**What**

The worktree removal and creation code now walks each directory component in a path without following symlinks, refusing to write into or remove a directory if any ancestor is a symlink (`O_NOFOLLOW` at every level). Rootless worktrees (agent directories that lost their git root) are now handled with a dedicated safe-remove path.

**Details**

- New `assertDirChainReal` function raises `ELOOP`/`ENOTDIR` at the first symlinked component
- `removeAgentWorktree` now handles the rootless case (no git root) separately: verifies the directory is inside `.claude/worktrees/`, is not a git repo, and requires an explicit `job_delete_force` source if the directory has any files
- Hook-managed worktrees check for symlinks in the stored worktree path before invoking the remove hook
- Error messages distinguish: "stored worktree path is a symlink", "a component of the stored worktree path is a symlink", "path could not be verified — its resolution changed while being checked"

**Evidence**

Path verifier (search for `"assertDirChainReal: dir must be strictly inside base"` or `"Refusing to write under symlinked or non-directory path"`). Rootless remove (search for `"Removed agent worktree with no git root at:"`).

### FileHistory Rewind: Atomic Restore with Symlink Safety

**What**

The file-history rewind (checkpoint restore) function now uses low-level file descriptors with `O_NOFOLLOW` to safely read backup files and write destination files, detecting symlink injection, hard links, FIFO substitution, and identity changes during the restore.

**Details**

- Backup source is opened with `O_RDONLY | O_NOFOLLOW | O_NONBLOCK`; `ELOOP`/`EMLINK` means the backup became a symlink — restore is refused
- Destination is opened with `O_WRONLY | O_CREAT | O_NOFOLLOW | O_NONBLOCK`
- Checks `nlink > 1` and refuses to overwrite hard-linked files
- Verifies `/proc/self/fd/<n>` path matches the expected parent directory (Linux only)
- Re-verifies `dev`/`ino` identity after opening, before truncating and writing
- Returns `{ filesChanged, skippedLinks }` instead of just an array; skipped-links count is surfaced to the caller

**Evidence**

Restore implementation (search for `"FileHistory: [Rewind] Refusing to delete"` or `"destination is hard-linked"`).

### Session Fork: More Specific Error Messages

**What**

When a session cannot be forked, the error now gives a specific reason rather than a generic refusal.

**Details**

- Persistence off: "Can't fork: session persistence is off, so the new session would have nothing to start from."
- Restricted flags: "Can't fork: this session was started with launch flags (safe or bare mode, a custom system prompt, a tool allowlist, or restricted settings)..."
- Nothing to fork yet: "Nothing to fork yet. Send a message first."
- Messages have also been shortened and made direct (no more parenthetical asides about worktrees or agent view)

**Evidence**

Fork error messages (search for `"Can't fork: session persistence is off"` or `"Can't fork: this session was started with launch flags"`).

### Skill Creation Context Tailored by Session Type

**What**

Claude now receives session-appropriate instructions about skill creation rather than a single generic message, covering four cases depending on what tools the session has.

**Details**

- Sessions with `save_skill` tool available: "Skill files on disk are a read-only cache: editing them does not change the user's saved skill. To create or update a skill, use the `save_skill` tool."
- Sessions with skill-proposal capability: "propose it with the `[propose_skill]` tool."
- Sessions with skill-delivery capability: "write it as a `.skill` file … and send it with the `[deliver_skill]` tool."
- All other sessions: "You cannot create or modify skills in this session. … say you can't do that here and point the user to their claude.ai settings."
- Plugin skills are an exception in all cases: customise through the `cowork-plugin` skill if available.

**Evidence**

Skill-saving system prompt (search for `"# Saving skills"` or `"Skill files on disk in this session"`).

### MCP OAuth: Revoke Replaced Tokens at Logout

**What**

When an MCP server's OAuth tokens are replaced (e.g., during re-authentication or token refresh), the old access and refresh tokens are now explicitly revoked at the server's `revocation_endpoint` before they are discarded.

**Details**

- Discovers the revocation endpoint from OAuth server metadata
- Supports both `client_secret_basic` and `client_secret_post` authentication methods
- Revokes refresh token first, then access token
- Logs failures with `Failed to revoke refresh token:` / `Failed to revoke access token:` but does not block the replacement

**Evidence**

Token revocation (search for `"Failed to revoke replaced tokens:"` or `"No replaced tokens to revoke"`).

### Context Over-Limit Warning Improved

**What**

The message shown when the context window is exhausted now distinguishes between the auto-compaction window and the raw model limit, and always names the specific action to take.

**Details**

- Auto mode: `Context exceeds the N-token limit by M tokens — run /compact or /clear to continue.`
- Non-auto mode: `Context is M tokens past the N-token compaction window — run /compact to reduce usage.`
- If `DISABLE_COMPACT` is set: suggests only `/clear`

**Evidence**

Context limit message (search for `"Context exceeds the"` or `"Context is"` combined with `"token compaction window"`).

### CCR Agent Proxy: Selective Relay Mode

**What**

New environment variables give fine-grained control over which hosts the CCR agent proxy relays to, allowing direct dial for selected hosts while tunnelling everything else.

**Details**

- `CCR_AGENT_PROXY_RELAY_MODE` — set to `selective` to only tunnel traffic to hosts in the include list; all others use direct networking
- `CCR_AGENT_PROXY_INCLUDE_HOSTS` — comma-separated list of hosts that the selective relay tunnels; unlisted hosts use normal networking
- `CLAUDE_CODE_AGENT_PROXY_GH_SHIM` — enable a GitHub CLI shim inside the agent proxy environment
- `CLAUDE_CODE_AGENT_PROXY_GIT_CONFIG` — pass a git config string to the agent proxy environment
- A startup reachability probe is logged as `[agent-proxy] startup reachability probe: ok`

**Evidence**

Selective relay log messages (search for `"[agent-proxy] selective relay:"` or `"CCR_AGENT_PROXY_RELAY_MODE=selective but include-host"`).

## Bug Fixes

- Permission stream failures now produce distinct, specific error strings instead of a single generic failure: `"tool permission stream closed before response received"`, `"canUseTool returned a schema-invalid permission result"`, `"tool permission request failed"`, `"tool permission request aborted"`. Evidence: (search for `"tool permission stream closed before response received"`)

- UTF-8 BOM (byte order mark, `﻿`) is now stripped from file content at the point it is read, preventing it from appearing in tool results and diff output. Evidence: (search for `"charCodeAt(0) === 65279"`)

- Sessions interrupted by daemon shutdown are now tagged `{ kind: "interrupted_turn" }` separately from other interruption types, enabling the host UI to show a different recovery prompt. Evidence: (search for `"interruptedByShutdown"`)

- Model routing telemetry (`tengu_refusal_fallback_route_declined`) now reports cascade chain details: `chain_length`, `chain_entry_index`, and `chain_entry` on unresolvable chain entries, giving better visibility into multi-hop fallback failures. Evidence: (search for `"chain_entry_unresolvable"`)

- The `removeAgentWorktree` function no longer requires a git root when the worktree path is rootless; it now delegates to the new `UZt` safe-remove path rather than returning a `"no git root for the worktree"` hard error. Evidence: String removed from old version (search for `"Removed agent worktree with no git root at:"` in new).

- Worktree-isolated agent shell commands that redirect git into the shared checkout are now blocked with a clear explanation: "Refusing to run it — a worktree-isolated agent's git operations must target its own worktree." Evidence: (search for `"commands from a worktree-isolated agent must run inside its worktree"`)

- MCP OAuth token revocation errors (`Failed to revoke replaced tokens:`) are now caught and logged without blocking the token replacement flow. Evidence: (search for `"Failed to revoke replaced tokens:"`)
