Channels: support user-scope allowlist + headless --dangerously-load-development-channels

Resolved 💬 3 comments Opened May 11, 2026 by zanacuadra Closed May 15, 2026

Feature Request — Channels plugin allowlist: user-scope + headless dev

To: Anthropic (Claude Code team)
From: Nico Cuadra (nicocuadrah@gmail.com) — operator of the Marine Farm Asia office, single-user Mac mini deployment running Claude Code as a daemon channel host (Telegram bridge for ERP automation).
Filed: 2026-05-11
Author of diagnostic: MF-DEV agent (Constructor) under Diagnose protocol
Claude Code version tested: 2.1.138 (/Users/nicolas/.local/share/claude/versions/2.1.138)

---

Summary

We forked the official telegram@claude-plugins-official channel plugin (0.0.6) into a local Marine Farm fork (pulpo-channel) to add operator-specific features: Decision Engine wiring, JSONL bus, cross-office broadcast, custom mitigations (allowlist drop log, attachment cap with 3 barriers, sanitize tests, flock JSONL outbox/inbox). We cannot put this fork on a public marketplace — the fork contains operator-specific paths, integration with our internal Decision Engine, and business logic that does not belong on a public registry.

We discovered through experimentation that the current Claude Code 2.1.138 channel runtime makes it effectively impossible to run a non-marketplace channel plugin in a headless / unattended daemon context without either (a) MDM-level system policy install, or (b) accepting an interactive trust prompt at every restart. This blocks the cutover from the official plugin to our fork.

This document explains the two specific gaps we hit and the two minimal API surfaces we believe would unblock single-user / per-machine deployments without weakening the security model that Anthropic clearly intends here.

---

Context — what we are trying to do

Single Mac mini running unattended as a 24/7 Telegram bridge for a small business. The plugin loads via pulpo-hub-start.sh from a tmux session and stays up across reboots via launchd + a */5 cron watchdog. The bot needs to come back from crashes, OS updates, and certificate renewals without a human at the keyboard to confirm prompts. That is the entire reason the deployment exists.

We want to replace telegram@claude-plugins-official with our local fork pulpo@pulpo-marketplace and keep this property.

---

Specific behavior we observed

Observation 1 — --channels plugin:<name>@<marketplace> requires the plugin to be in an allowlist that is only readable from managed settings.

With the plugin installed locally (claude plugin marketplace add /path/to/local-marketplace + claude plugin install pulpo@pulpo-marketplace --scope local), the boot is clean — no trust prompt — but Claude Code logs:

plugin:pulpo@pulpo-marketplace · not on the approved channels allowlist

The MCP server itself comes up correctly (bun server.ts starts, bot.pid is written, Telegram polling runs, gate drops are logged into drops.jsonl). The plugin is functional as a server. What gets dropped is the channel routing: notifications/claude/channel events emitted by the server never reach the model, because the receiving side rejects them before that.

The error message in the binary (visible via strings) is explicit:

is not on your org's approved channels list (set allowedChannelPlugins in managed settings)

We tried setting allowedChannelPlugins and channelsEnabled in ~/.claude/settings.json (user scope). The setting is silently ignored. Confirmed by a second run with the same warning. The binary contains the description string Only read from policy settings (managed-settings.json / MDM) near related config code, which matches the runtime behavior.

The corresponding path is /Library/Application Support/ClaudeCode/managed-settings.json on macOS — writing it requires sudo, and the file is system-wide rather than per-user. For a managed-enterprise deployment that is correct. For a single-user Mac mini it is heavy: it asks the operator to elevate to root and write a system-wide policy file for what is, from their perspective, a per-user preference about their own Claude session.

Observation 2 — --dangerously-load-development-channels always opens an interactive trust prompt, with no headless override.

The escape hatch the binary suggests is:

plugin pulpo@pulpo-marketplace is not on the approved channels allowlist
(use --dangerously-load-development-channels for local dev)

We tested this with the plugin both uninstalled and already installed via claude plugin install, and in both cases the boot pauses on an interactive selector:

WARNING: Loading development channels
--dangerously-load-development-channels is for local channel development only.
Do not use this option to run channels you have downloaded off the internet.
Channels: plugin:pulpo@pulpo-marketplace
❯ 1. I am using this for local development
  2. Exit
Enter to confirm · Esc to cancel

The prompt is gated on a TTY and there is no --yes-style accept flag we could find. We grep'd the binary for CLAUDE_CODE_*SKIP*, *BYPASS*, *ALLOW*, *APPROVE*, *DEV*, *CHANNEL*, *WARNING*, *DANGEROUS* and tried six plausible env-var candidates against the running binary; none of them disabled the prompt. Confirmed against claude 2.1.138 strings: the only env vars in the --dangerously-load-development-channels codepath are unrelated.

Our pulpo-hub-start.sh already auto-accepts the workspace trust dialog ("trust this folder|trust this project" regex on tmux pane captures). We could in principle extend that to auto-press Enter on the development-channels prompt, but we would rather not — a hacky pane-capture-and-send-keys driver is exactly the kind of automation that breaks silently when Anthropic tweaks the wording in a future release, and the failure mode (waiting forever at boot) is the worst possible failure mode for a watchdog-supervised daemon.

Sanity check — both observations together

The two gates compose in a way that is hostile to unattended single-user use:

  • Without the dev flag → boot is clean but routing is dropped (allowlist).
  • With the dev flag → routing works but boot blocks on a prompt.

There is no headless path through both gates without writing /Library/Application Support/ClaudeCode/managed-settings.json as root — i.e. pretending to be a managed-enterprise deployment when we are not one.

---

Asks (two minimal API surfaces)

We are deliberately keeping these small and additive. Both fall back to current behavior when the new surface is not set.

Ask 1 — Honor allowedChannelPlugins from user-scope settings.json

Allow channelsEnabled and allowedChannelPlugins to be read from ~/.claude/settings.json in addition to managed-settings.json. When both are present, managed settings win (current precedence preserved for enterprise). When only user settings are present, the user-scope allowlist applies to that user only.

Conceptually:

// ~/.claude/settings.json (user-scope)
{
  "channelsEnabled": true,
  "allowedChannelPlugins": ["pulpo@pulpo-marketplace"]
}

This is the minimum change that lets a single-user Mac mini run its own local channel plugin without sudo. The security argument for keeping this MDM-only is essentially "an attacker who can write the user's ~/.claude/settings.json can grant a malicious plugin channel access" — but that same attacker can already replace ~/.claude/settings.json to enable any plugin, register any marketplace, change the model, etc. The user-scope settings file is already the trust boundary for the user's own session; treating channels as a special case here costs unattended single-user deployments much more than it gains in adversary capability.

If you would prefer to keep the default tight, gate this behind an explicit opt-in: e.g. channelsAllowUserScope: true in managed-settings.json (default false), so the enterprise admin still controls whether a managed Mac can grant itself local channel access.

Ask 2 — Add --skip-development-channels-warning (or env-var equivalent) for headless dev

Add a flag — exact name negotiable — that suppresses only the interactive selector in --dangerously-load-development-channels, equivalent to choosing option 1. I am using this for local development. The warning text can still print to stderr. The prompt cannot block.

Example invocation we would use from pulpo-hub-start.sh:

claude \
  --plugin-dir /path/to/pulpo-channel \
  --dangerously-load-development-channels=plugin:pulpo@pulpo-marketplace \
  --channels plugin:pulpo@pulpo-marketplace \
  --skip-development-channels-warning \
  --permission-mode bypassPermissions

Or as an env var: CLAUDE_CODE_SKIP_DEV_CHANNELS_WARNING=1. Either works for our use case; the env var form is slightly more convenient for launchd-managed daemons but a flag is fine.

We acknowledge this lowers the "did the human actually read this" friction — that is the point. The friction is what makes the flag unusable for CI, headless daemons, and any unattended boot. The flag would be the operator's explicit, written acknowledgement; the warning would still appear in the stderr stream that the operator is responsible for monitoring. This mirrors how --dangerously-skip-permissions already works: dangerous, explicit, opt-in, but headless-capable.

---

What we are doing in the meantime

We are deferring our cutover to pulpo-channel. The official telegram@claude-plugins-official plugin continues to run. The fork stays on its dedicated branch (agent/mf-dev/2-v1-5-pulpochannel-paso-b-fix-fase5 based on 010a01e) with the marketplace template preserved under /tmp/diag-fase5/pulpo-marketplace/ for the eventual cutover. The diagnostic that led to this filing is indexed in our Qdrant memory under topic:diagnostico-mf-dev and topic:channels-plugins-api (lessons cross-applicable to any future operator hitting the same wall).

Happy to give you whatever additional logs, repro scripts, or strings output would be useful. The bun server.ts half of this works perfectly — full credit to the team for how clean the channel server API is. The two asks above are entirely about how a single-user operator gets the channel runtime to honor a fork.

Thanks.

---

Appendix — repro for asks 1 and 2

# Ask 1 — confirm user-scope ignored
jq '. + {"channelsEnabled": true, "allowedChannelPlugins": ["pulpo@pulpo-marketplace"]}' \
  ~/.claude/settings.json > /tmp/s.json && mv /tmp/s.json ~/.claude/settings.json

claude plugin marketplace add /tmp/diag-fase5/pulpo-marketplace
claude plugin install pulpo@pulpo-marketplace --scope local

# Inside a real TTY pane:
claude --channels plugin:pulpo@pulpo-marketplace --permission-mode bypassPermissions

# Expected (if Ask 1 honored): no allowlist warning. Actual: still prints
# "plugin:pulpo@pulpo-marketplace · not on the approved channels allowlist".
# Ask 2 — confirm no env override of the dev prompt
for v in CLAUDE_CODE_SKIP_DEV_CHANNELS_WARNING CLAUDE_CODE_AUTO_APPROVE_DEV_CHANNELS \
         CLAUDE_CODE_ALLOW_DEV_CHANNELS    CLAUDE_CODE_CHANNELS_BYPASS_ALLOWLIST \
         CLAUDE_CODE_BYPASS_DEV_WARNING    CLAUDE_CODE_DISABLE_POLICY_SKILLS ; do
  echo "--- $v=1 ---"
  $v=1 claude \
    --plugin-dir /path/to/pulpo-channel \
    --dangerously-load-development-channels=plugin:pulpo@pulpo-marketplace \
    --channels plugin:pulpo@pulpo-marketplace \
    --permission-mode bypassPermissions
done
# Expected: at least one suppresses the selector. Actual: all six still gate
# on "1. I am using this for local development".

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗