tutorials #claude-code #managed-settings #http-hooks #allow-list #coding-agents #change-control

Treat Unreadable Managed Settings as Empty

When a managed allow-list cannot be read, Claude Code now admits nothing. Classify the block, name who owns the file, then resume the coding agent.

Standup hears “the org locked HTTP hooks.” Someone pasted a red agent log. The hook URL that worked yesterday is blocked. The junior opens the vendor status page. The coding agent offers to “relax the allow-list in the project file until chat works.”

I stop the run there. A sudden block on HTTP hooks is a readable-file problem until proven otherwise. It is not “Claude Code locked the company.”

On 9 September 2026 Claude Code published a patch that names three managed allow-list keys. Official notes say managed allowedHttpHookUrls, httpHookAllowedEnvVars, and allowedChannelPlugins now admit nothing, not everything, when those settings are unreadable. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267] [Source: https://code.claude.com/docs/en/changelog]

I already refused to treat an Organization policy why-line as a loaded fleet file in Read the Why-Line Before You Trust the Org Policy. I already refused to treat a failed Cloudflare login as a status outage in Read the Requested Scopes Before You File an Outage. This post is the same desk rule for the managed allow-list. Read the file. Parse the three keys. Name who owns that file. Do not let the agent rewrite policy as a drive-by lint.

The question is not whether the CLI printed a new block. The question is whether the managed file on this machine is readable, and who owns the pin when it is not.

Classify the red hook: not a lockdown, read the managed file, name the owner

The red hook that looks like a lockdown

Juniors read a blocked HTTP hook the way they read a 403 from the public API. Red text. The word “allow.” A URL that used to fire. They assume security shipped a new deny.

Allow-lists do not work that way. Unset means any URL is allowed. An empty array blocks every HTTP hook. A defined array runs a hook only when the URL matches. Official settings docs spell that default in one sentence: when allowedHttpHookUrls is unset, any URL is allowed; when you define the key, non-matching URLs are blocked without running; an empty array blocks every HTTP hook. [Source: https://code.claude.com/docs/en/settings-reference]

The 9 September patch changes what happens when the managed value cannot be read. The GitHub notes and the changelog use the same sentence: those three keys admit nothing, not everything, when unreadable. “Everything” is the old default for an unset allow-list: any URL, any hook env name, the default channel-plugin list. “Nothing” is the empty-array end of the same docs. Read that bullet as a parse ticket, not as a new deny that security shipped overnight. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267] [Source: https://code.claude.com/docs/en/changelog]

Three sentences belong on the ticket, in this order:

  1. Which managed file or source Claude Code selected (remote, plist, file, drop-ins, or a merged list).
  2. Whether allowedHttpHookUrls, httpHookAllowedEnvVars, or allowedChannelPlugins is unreadable as a whole, or only one entry is bad.
  3. Who owns that file, and whether the owner will repair it before anyone “unblocks” hooks in a user settings file.

If you skip sentence two, you will file a security incident for a JSON parse, or you will open the allow-list in the wrong file.

I do not invent a fake lockdown on this desk. I use the public contract. The GitHub release is not a prerelease. Published 9 September 2026 at 19:58 UTC. The bullet is explicit: admit nothing, not everything, when unreadable. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

Three keys, three owners

Laravel and Vue work on this desk still runs coding agents next to HTTP hooks: a deploy bot that posts to an internal URL, a header that interpolates a token, a channel plugin that pushes a build note. Mixing those three into one “hooks are down” thread is how a junior turns a parse error into a two-hour security page.

KeyWhat it gatesUnsetEmpty arrayUnreadable managed value after 9 September
allowedHttpHookUrlsWhich URLs HTTP hooks may callAny URLBlock every HTTP hookEmpty allow-list until fixed
httpHookAllowedEnvVarsWhich env names may land in hook headersEach hook’s own listNo header interpolation from envEmpty allow-list until fixed
allowedChannelPluginsWhich channel plugins may push inbound messagesDefault Anthropic listBlock all channel pluginsEmpty allow-list until fixed

Do not copy the version-pin fallback onto these three keys. Official managed-settings docs still say requiredMinimumVersion and requiredMaximumVersion fail open on purpose: an invalid value is dropped rather than enforced. A different row on the same page, allowedMcpServers, already enforces an empty allow-list when the value is present but invalid. The 9 September bullet is the HTTP-hook and channel-plugin cousin of that instinct, sourced from the release notes, not from that table. [Source: https://code.claude.com/docs/en/managed-settings] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

allowedChannelPlugins is managed-only. allowedHttpHookUrls and httpHookAllowedEnvVars can live in any settings file and merge across sources. The HTTP hook allow-lists apply to hooks from every source, including managed policy. [Source: https://code.claude.com/docs/en/hooks] [Source: https://code.claude.com/docs/en/settings-reference]

Do not paste one screenshot and call every row done. If the red line names a URL, you are on the HTTP hook row. If it names a header env var, you are on the interpolation row. If it names a channel plugin id, you are on the channel row. Write the key name first.

npm tags are evidence, not the hook
This morning’s registry, 10 September 2026: @anthropic-ai/claude-code latest is 2.1.267, published 9 September 2026 at 18:25 UTC. GitHub v2.1.267 published 9 September 2026 at 19:58 UTC, prerelease false. The stable dist-tag is still 2.1.236, published 19 August 2026. Pin what you run. Do not put those numbers in the title. Do not treat stable as the field pin. [Source: https://www.npmjs.com/package/@anthropic-ai/claude-code] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

Fail closed is a ticket type

Juniors hear “invalid JSON” and think “ignore that key and keep going.” For these three managed keys, the 9 September notes say the opposite when the value is unreadable: admit nothing.

Official admin docs still split other invalid managed values into two jobs. Most managed keys drop the bad item, log a warning, and keep the rest of the policy. A short list of enforcement keys is not dropped when invalid. Claude Code enforces a stricter fallback until the value is fixed. allowedMcpServers is on that list. The three hook and channel keys are named in the release notes for the unreadable case. Put the GitHub bullet on the ticket next to doctor, not a reconstructed table. [Source: https://code.claude.com/docs/en/managed-settings] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

User, project, and local settings files stay strict. A file whose JSON or top-level shape fails validation is rejected as a whole and reported. An individual bad permission rule is skipped with a warning while the rest of that file applies. That tolerance sentence on the docs page applies only to managed settings. Do not diagnose a broken ~/.claude/settings.json with the managed fail-closed table. [Source: https://code.claude.com/docs/en/managed-settings]

I keep a short table on the wiki card. I do not let the agent expand it into a bypass novel.

What you sawWhat it isNext action
HTTP hooks blocked after a managed-file deployFail-closed empty allow-list, or a real empty arrayRun claude doctor. Repair the managed file. Do not widen the user file.
File parses, one URL pattern looks garbageUnknown until doctor names the fieldCopy doctor. Do not delete the whole key to “unblock.”
/status Organization policy why-line, hooks still fireLoad miss, not this patchRead the why-line post. Name the proxy owner.
requiredMinimumVersion ignoredFail-open by designFile against the version-pin owner, not against hooks.

Three allow-list ends: unset allows, empty blocks, unreadable admits nothing

Copy doctor, then classify it

Do not screenshot a cropped window. Copy the command, the binary version, and the doctor lines that name dropped keys. Then classify.

 1#!/usr/bin/env bash
 2set -euo pipefail
 3
 4out=${1:-/tmp/claude-doctor.txt}
 5bin=$(command -v claude || true)
 6
 7{
 8  echo "host=$(hostname)"
 9  echo "user=$(id -un)"
10  echo "claude_bin=${bin:-missing}"
11  command -v claude >/dev/null && claude --version || true
12} | tee /tmp/cli-pin.txt
13
14claude doctor >"$out" 2>&1 || true
15
16if grep -Ei 'allowedHttpHookUrls|httpHookAllowedEnvVars|allowedChannelPlugins' "$out"; then
17  echo "STOP: managed allow-list keys named. File the settings owner, not a lockdown."
18  exit 2
19fi
20
21if grep -Ei 'unreadable|could not be (read|parsed)|invalid' "$out"; then
22  echo "STOP: unreadable or invalid managed value. Empty allow-list until fixed."
23  exit 2
24fi
25
26echo "doctor did not name the fail-closed keys. Keep the URL, the header, and the plugin id on the ticket."

Run that script on the machine that blocked the hook. Do not run it on a laptop that never loaded the fleet file and then declare the fleet healthy.

claude doctor is the documented place that lists removed items, their source file, and the field. Official managed-settings docs say to run it when managed settings contain entries that fail the schema. [Source: https://code.claude.com/docs/en/managed-settings]

If doctor names a proxy miss, you are back on the why-line ticket. That is a load problem. This post is a parse problem. I already wrote how to read that why-line. Do not merge the two threads.

Prove the file before you touch the allow-list

A named owner needs a file path, a parse result, and the three keys. Guessing the path from chat is how you “fix” a file the CLI never read.

Linux and WSL file source: /etc/claude-code/managed-settings.json. macOS file source: /Library/Application Support/ClaudeCode/managed-settings.json. Pass another path as argv when MDM or a drop-in directory is the source. [Source: https://code.claude.com/docs/en/managed-settings]

 1#!/usr/bin/env python3
 2import json
 3import sys
 4from pathlib import Path
 5
 6FAIL_CLOSED = (
 7    "allowedHttpHookUrls",
 8    "httpHookAllowedEnvVars",
 9    "allowedChannelPlugins",
10)
11candidates = [
12    Path("/etc/claude-code/managed-settings.json"),
13    Path("/Library/Application Support/ClaudeCode/managed-settings.json"),
14]
15path = Path(sys.argv[1]) if len(sys.argv) > 1 else next((p for p in candidates if p.exists()), None)
16if path is None:
17    print("NO_FILE")
18    sys.exit(2)
19
20raw = path.read_bytes()
21print(f"path={path}")
22print(f"bytes={len(raw)}")
23try:
24    data = json.loads(raw.decode("utf-8"))
25except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
26    print(f"UNREADABLE:{type(exc).__name__}:{exc}")
27    print("CLASS=fail_closed_empty_allowlist")
28    sys.exit(3)
29
30if not isinstance(data, dict):
31    print("UNREADABLE:top_level_not_object")
32    print("CLASS=fail_closed_empty_allowlist")
33    sys.exit(3)
34
35for key in FAIL_CLOSED:
36    if key not in data:
37        print(f"{key}=unset")
38        continue
39    val = data[key]
40    if not isinstance(val, list):
41        print(f"{key}=invalid_type:{type(val).__name__}")
42        print("CLASS=fail_closed_empty_allowlist")
43        continue
44    print(f"{key}=list_len_{len(val)}")

The script exits 2 when none of the official file paths exist, 3 when the bytes will not parse. Unreadable bytes are the new “admit nothing.” They are not “key missing, so allow all.”

I keep this next to the Laravel deploy notes on purpose. A PHP config cache that will not parse does not mean “no config, so public.” The 9 September bullet puts the same instinct on three managed keys. User settings files already rejected a broken file as a whole. Do not treat a managed parse miss as an unset key.

Copy doctor, then the official file path, then the named owner

Do not delete the key to unblock

The other trap is the opposite of unreadable. The file parses. One URL pattern looks wrong. The junior deletes the whole allowedHttpHookUrls array “to unblock.”

The 9 September bullet names the unreadable case. It does not give you a license to unset a readable key. Unset is the “any URL” default in the settings reference. Deleting the key is how you open the list. Leave the readable list in place until the named owner edits the managed source. [Source: https://code.claude.com/docs/en/settings-reference] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

Classify with a second script. Do not ask the agent to “clean the file.”

 1#!/usr/bin/env python3
 2import json
 3import sys
 4from urllib.parse import urlparse
 5
 6path = sys.argv[1]
 7data = json.loads(open(path, encoding="utf-8").read())
 8urls = data.get("allowedHttpHookUrls")
 9if urls is None:
10    print("unset: any URL allowed at this file (other sources still merge)")
11    sys.exit(0)
12if urls == []:
13    print("empty_array: every HTTP hook blocked by this file")
14    sys.exit(2)
15if not isinstance(urls, list):
16    print("invalid_type: fail closed on managed source")
17    sys.exit(3)
18
19bad = []
20for item in urls:
21    if not isinstance(item, str) or not item:
22        bad.append(repr(item))
23        continue
24    parsed = urlparse(item.replace("*", "x"))
25    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
26        bad.append(item)
27
28print(f"count={len(urls)} bad={len(bad)}")
29for item in bad:
30    print(f"STRIP_CANDIDATE:{item}")
31if bad:
32    sys.exit(4)

This script does not claim Claude Code uses urlparse. It gives the named owner a list of strings a human can compare to the official pattern examples: https://hooks.example.com/* and http://localhost:*. Hostname matching is case-insensitive and treats a trailing DNS dot as the same host. [Source: https://code.claude.com/docs/en/settings-reference]

If the owner needs a bypass, that is a policy ticket. It is not a coding-agent ticket. I do not publish a recipe that widens the list.

What the coding agent is allowed to do

The agent’s first idea is always the same: edit the nearest settings file until the hook fires. That is how you launder a managed fail-closed into a user fail-open.

Allowed:

  1. Run claude --version and claude doctor.
  2. Copy the three key names and the source file doctor printed.
  3. Stop, and ping the named owner of the managed file.

Forbidden:

  1. Write allowedHttpHookUrls in the project or user file to “match production.”
  2. Unset the key because “empty is too strict.”
  3. Retry the hook ten times and call the API dead. A different 9 September bullet fixed expired AWS or Google Cloud credentials under a host app that retried ten times with a generic “request failed.” That is a re-auth ticket, not this allow-list ticket. Keep them on separate cards. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

I already refused to leave Copilot Approve on as a required check. The same instinct applies here. The model that wrote the PR does not own the fleet allow-list. See Leave Copilot Approve Off if your team still treats agent review as a merge gate. For the broader agent-ops habit, start at /ai-agent-operations/.

Coding agent may copy doctor and must not widen the allow-list

Merge is not a reason to skip the owner

Arrays merge across settings files. That sentence is in the official allowedHttpHookUrls reference. Juniors hear “merge” and think a user file can punch a hole in a managed empty list.

Read the managed page with the settings-reference page together. HTTP hook allow-lists apply to hooks from every source. When the managed value is unreadable, the release notes say admit nothing. Repair the managed source doctor named. A user file that lists https://hooks.example.com/* is not the repair. [Source: https://code.claude.com/docs/en/settings-reference] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267]

allowManagedHooksOnly is a different switch. When true, only managed hooks (plus SDK hooks and hooks from plugins force-enabled in managed enabledPlugins) run. User, project, and local hooks are blocked. That switch is managed-only. Do not diagnose a fail-closed allow-list as allowManagedHooksOnly unless doctor named that key. [Source: https://code.claude.com/docs/en/settings-reference]

I write the owner on the ticket as a person, not a role. “Platform” is how these files stay unreadable until Friday.

What you should do Monday morning

  1. Print claude --version on the machine that runs HTTP hooks. If it is older than the 9 September fail-closed patch, write the pin owner on the ticket. Do not treat npm stable 2.1.236 as the field.
  2. Run claude doctor. Save the Organization policy line and any line that names allowedHttpHookUrls, httpHookAllowedEnvVars, or allowedChannelPlugins.
  3. If doctor says the managed value is unreadable or invalid, classify the ticket as fail-closed empty allow-list. Stop the coding agent. Do not widen a user or project allow-list.
  4. If the file parses, leave the list in the managed source. Do not delete the key. File the odd pattern with the owner.
  5. Name one human who owns the managed-settings file (or the MDM/plist/remote source). Put that name on the wiki card next to the CLI pin.
  6. Keep the why-line post and this post on separate cards. A proxy that drops the settings endpoint is a load miss. Unreadable bytes are a parse miss. Both can print the word “policy.” They are not the same repair.

The question is not whether this demos well in chat. The question is whether the allow-list survives maintenance, handoff, and a file that will not parse.

Further reading

Source Claude Code v2.1.267 release notes (fail-closed allow-list bullet)

Source Deploy managed settings — keys that fail closed

Source allowedHttpHookUrls and httpHookAllowedEnvVars reference