Skip to content

Plugin-bundled context MCP server: load_context(key) for subagent-scoped reference content #26

@krisrowe

Description

@krisrowe

Status (2026-04-19): prerequisites resolved, design confirmed, ticket clear to implement

Both open prerequisites from the upstream investigation are now
closed empirically. Summary of the research verdict so this
ticket's readers don't have to chase the separate research repo:

  • Mechanism Update debug-agent-tests skill: log review as verification on every run #3 (asymmetric built-in-tool grants via agent
    frontmatter) — closed as architecturally not viable. Claude
    Code subagents inherit parent permissions with additional
    tool-type restrictions only; there is no documented form of
    per-agent path-scoped tool grants. Confirmed both by
    documentation and empirical test.
  • Mechanism Agent interface contract: parent-facing skill, schema enforcement, and discovery #4 (key embedded in subagent .md body) —
    confirmed confidential in the plugin-distributed case. Plugin
    install paths live outside the user's project cwd, and the
    parent's default Read is cwd-bounded, so the bundled
    subagent's .md isn't reachable to the parent without an
    explicit grant that the user would have to consciously set.
    No additional project-level path-deny hardening required for
    the shippable plugin-distributed form. (Path-deny hardening
    is only relevant for the degraded project-scope packaging
    variant.)
  • Mechanism Schema redesign: SARIF-aligned file findings, ruleId foreign keys, local-only scope #5 (plugin-bundled capability-broker MCP with
    key in subagent body) — empirically validated end-to-end.
    Plugin loads, bundled MCP server spawns, bundled subagent
    invokes load_context(key), broker returns the mapped
    content, parent sees it via the subagent's reply. Parent
    without a valid key cannot extract content. Chained-attack
    integration test (parent tries to Read the subagent .md to
    extract the key) was blocked by the cwd boundary without any
    extra hardening.
  • Mechanism CI/CD integration: privacy scanning in GitHub Actions for PRs #6 (hook-based caller-identity enforcement via
    parent_tool_use_id) — closed as not implementable with
    current Claude Code primitives. PreToolUse hook stdin payload
    carries session_id, transcript_path, cwd,
    permission_mode, hook_event_name, tool_name,
    tool_input, tool_use_id — NOT parent_tool_use_id.
    The hook layer has no way to distinguish parent-scope from
    subagent-scope invocations using the standard stream-json
    scope signal. Mechanism Schema redesign: SARIF-aligned file findings, ruleId foreign keys, local-only scope #5 stands without it.

Shippable recipe for this ticket (minimal, no degraded
variants):

  1. Plugin bundles: .claude-plugin/plugin.json, .mcp.json
    wiring ${CLAUDE_PLUGIN_ROOT}/server.py, server.py
    exposing load_context(key) -> str reading a user-XDG
    key→.md-file map, hooks/hooks.json for the SessionStart
    pip install -t ${CLAUDE_PLUGIN_DATA}/site-packages
    pattern (canonical, from claude-plugin-creator), and
    agents/<subagent>.md with its specific key embedded in
    the body.
  2. Settings (user-scope, project-scope, or plugin-shipped —
    verify which carries by running the installed plugin)
    include permissions.allow: ["mcp__plugin_<plugin>_<server>__*"]
    so the subagent's tool invocation isn't gated on an
    interactive approval that doesn't arrive in -p mode.
  3. No additional path-deny hardening required for the
    plugin-distributed form. The cwd boundary is doing the work.

The remaining implementation work is writing the plugin's
server.py, the key-map schema, the subagent .md, and
verifying the settings grant lands at an appropriate scope for
users installing the plugin.

Problem

Some agents bundled with this plugin need access to configuration
or reference content that the plugin's orchestrating parent (and
any other agent in the session) must not see. The canonical
example is a scanning agent that needs a user-maintained catalog
of identifiers or patterns to look for in other artifacts; the
orchestrating parent never needs that catalog and must not end
up with it in its context window (where it would bleed into test
fixtures, code comments, commit messages, or other emissions).

The implementation mechanisms Claude Code documents for this
privilege-separation shape and their current status:

  • The most obvious mechanism — inline mcpServers: in a
    subagent's frontmatter — is documented but has open upstream
    bugs preventing reliable invocation
    (anthropics/claude-code#13898,
    #25200,
    #33689),
    and a companion limitation closed as not planned
    (#29655)
    — subagents do not receive MCP server instructions.
  • The workarounds suggested in those issue threads either
    defeat context isolation (move the MCP server to global,
    have the parent orchestrate the call) or require a whole
    different architecture (external MCP server that mediates
    agent + tool relationships).

So a plugin that wants to keep sensitive context out of the
parent's context window — but also wants to keep shipping as a
normal plugin that any user can install — has no clean
first-party mechanism today.

Proposed solution

Ship a small MCP server inside this plugin that exposes a
single tool for loading content by opaque key. Embed the key
that identifies a given agent's required context inside that
agent's .md body (not its frontmatter description, not
project settings, not anywhere the parent would naturally
read). The parent can technically call the tool, but without
the key it cannot pivot the tool into a useful lookup.

This turns a filesystem-permission question ("is the parent
blocked from reading this path?") into a capability-token
question ("does the caller know the right key?"), which is
strictly weaker but much more portable across environments
and does not depend on the broken-upstream features above.

Interface

Single tool, plain stdio MCP, bundled with this plugin:

load_context(key: str) -> str
  • key is a short opaque string. The server does not enumerate
    valid keys over the wire; there is no list_keys() tool.
    Valid keys are learned only by being embedded in agent
    definitions that are authorized to call this tool.
  • Returns the raw content of a Markdown file mapped to that
    key, or a structured error if the key is unknown, the
    mapped path is missing, or the mapped file is not a .md
    file.

Key → path mapping

The server reads a user-maintained config file at a
conventional location under the user's XDG config base (exact
path TBD at implementation; pick a location namespaced to this
plugin, e.g. $XDG_CONFIG_HOME/<namespace>/context-map.toml,
with ~/.config/ fallback when XDG_CONFIG_HOME is not set).

Config shape (sketch — finalize during implementation):

# Keys are opaque identifiers agents embed in their .md body.
# Paths may be absolute, home-relative (~/...), or relative to
# a conventional base directory.

[keys]
some-key-name = "~/some-relative/path.md"
another-key   = "/absolute/path/to/file.md"

Format restriction

The server loads ONLY files with a .md extension. This is a
defense-in-depth layer: even if the parent somehow learned a
valid key, it cannot pivot the tool into a general-purpose
filesystem read by pointing a key at /etc/passwd or similar.
Unknown extensions return a structured error.

Install / bundling

Bundled in this plugin using the direct-source + explicit-deps
pattern documented canonically in
echomodel/claude-plugin-creator/docs/plugin-patterns.md:
${CLAUDE_PLUGIN_ROOT}/server.py, deps installed via the
SessionStart hook into ${CLAUDE_PLUGIN_DATA}/site-packages by
diffing requirements.txt. No separate pipx install step for
users. Do not use uvx-based invocation — uv is not yet
ubiquitous on user machines and would be a portability regression.
Do not use an in-package version-string compare as the install
trigger (see the
antipattern section
of the patterns doc for the failure-mode analysis).

./agent script reconsideration (forced by this change)

This repo currently ships a ./agent script that supports
standalone installation of the privacy agents outside the plugin.
Introducing a bundled MCP server means that path — which today
covers agent .md files only — no longer gives standalone users
the full privacy tooling. Maintaining the ./agent path in
parallel with the bundled MCP server risks divergence and
maintenance cost that may not be worth it.

Decide at implementation time among:

  • Deprecate ./agent. Standalone users install via
    documented pipx install git+<url>@<tag> (or similar) once the
    MCP server ships with a pyproject.toml entry point. Simplest
    maintenance; drops a distribution path we built ad-hoc.
  • Extend ./agent to also register the MCP server
    alongside the agents. Preserves the existing UX; adds complexity
    and couples the script to the MCP install path.
  • Leave ./agent alone and accept that standalone users get
    the agents but not the MCP-backed features. Cheapest but
    silently degrades functionality for existing standalone users.

Record the decision and rationale in CONTRIBUTING.md and
README.md at the moment of implementation. Do not preserve the
path out of inertia; the default expectation is deprecation
unless a concrete user-facing reason to keep it is identified.

Who calls it

Only the plugin's own subagents that are specifically
authorized for privileged context lookups. Each such agent's
.md body contains the exact key it should call with. Other
agents in the plugin do not receive keys and therefore cannot
usefully invoke the tool even though they can see it in their
MCP catalog.

Why this is YAGNI on the broader concern

It is tempting to frame this as "a general-purpose capability
broker" and design the full Vault-style abstraction up front:
multiple namespaces, role-based access, key rotation, audit
logging, a bulk-import skill, a web-facing admin, etc. That
design space is real and interesting, but:

  1. We have exactly one concrete use case today — the
    scanning-agent-needs-PII-catalog pattern. Building for
    imagined future use cases of "shared config", "secrets
    broker", or "per-agent capability scoping" produces a
    larger and more abstract surface without serving any
    consumer we actually have.
  2. The interface as specified here is forward-compatible.
    A future evolution to richer semantics can layer on top:
    add namespace prefixes to keys, add new tools alongside
    load_context, move the key→path resolver into a shared
    library. Nothing in this design traps us.
  3. We are partially using this as a workaround to Claude
    Code product limitations
    , not as a considered
    architectural layer. Acknowledging that: we should not
    over-invest in an ecosystem-flavored design for what is, in
    part, a patch around upstream bugs. If those upstream issues
    close with fixes that enable the cleaner "inline-MCP-in-
    subagent-frontmatter" path, we may want to fold this MCP
    server back down or remove it entirely. Keeping the surface
    small now keeps that option open.

We acknowledge the broader general-purpose-context-broker
framing and may revisit it later. This ticket explicitly does
NOT attempt it.

Upstream issues driving this design — document and revisit

Because part of our rationale for building this MCP server is
that upstream Claude Code limitations block cleaner
alternatives, the upstream tickets that define those
limitations are load-bearing context. If any of them close
with a fix that enables the cleaner path, we should
reconsider whether to keep this MCP server at all, or fold it
down to a minimal bridge, or rip it out entirely.

CONTRIBUTING.md should carry a section that:

  1. Lists each upstream issue with title, link, current status
    as of the section's last update, and one-line summary of
    what it blocks for us.
  2. Explicitly notes this is a revisit list — when any of
    these tickets changes state (closes with a fix, gets a
    concrete design proposal, or gets declined as
    "not planned"), we should re-evaluate the design captured
    in this repo.
  3. Recommends a periodic cadence (e.g., quarterly, or any
    time a major Claude Code release lands) for a maintainer
    or agent to walk the list, update the statuses, and open
    a follow-up ticket here if any design decision becomes
    worth revisiting.
  4. Records the date and claude-code version of the most
    recent check so readers can tell how stale the listed
    statuses are.

The upstream issues to seed the list:

  • anthropics/claude-code#13898
    — "Subagents hallucinate instead of calling MCP tools from
    project-scoped servers." Blocks the inline-MCP-in-subagent
    path; if fixed, reconsider whether a plugin MCP server for
    context lookup is still needed.
  • anthropics/claude-code#25200
    — "Custom agents cannot use deferred MCP tools." Blocks
    per-agent tools: whitelisting of MCP tool names as a
    privilege-separation mechanism.
  • anthropics/claude-code#29655
    — "Subagents do not receive MCP server instructions."
    Closed "not planned"; an architectural limitation that
    shapes the workaround. If reopened and fixed, tool
    descriptions could carry their own invocation context
    without embedding keys in agent bodies.
  • anthropics/claude-code#33689
    — "Sub-agent-scoped MCP configs silently ignored in
    plugin-distributed agents." Blocks scoping an MCP server
    to a specific plugin agent via agent-file frontmatter.

If any of these ship fixes that enable the cleaner first-party
alternative, the follow-up work is to evaluate reverting this
ticket's code and simplifying to the now-working first-party
mechanism.

Prerequisite verdict (2026-04-19): asymmetric-tool-grant mechanism is architecturally dead — ticket proceeds as specified

The prerequisite below has been resolved. The
asymmetric-built-in-tool-grant mechanism is not viable —
empirically and per upstream documentation — so this ticket
proceeds as originally designed. Evidence:

Empirical result: an independent experimental workstream
tested a subagent whose frontmatter declared bare
tools: [Read] attempting to Read an outside-cwd file that
the parent could not Read. The subagent was denied with the
same cwd-boundary permission check the parent hit. The
subagent's tools: frontmatter field did NOT expand
filesystem reach — it is a capability allowlist (which tool
TYPES the subagent can use), not a path grant.

Documentation confirmation:

  • https://code.claude.com/docs/en/sub-agents
    "Each [subagent] inherits the parent conversation's
    permissions with additional tool restrictions."

    Subagents can only have LESS access than the parent via
    frontmatter, never more.
  • https://code.claude.com/docs/en/permissions — path-scoped
    Read(<path>) patterns are documented only for
    settings.json (session scope). No documented form of
    per-agent path-scoped tool permissions exists.

Untested variants (speculative tools: [Read(<path>)]
syntax, per-agent permissions.deny) are undocumented, and
relying on undocumented behavior for a security primitive is
inappropriate regardless of whether empirical probes might
succeed.

Decision: proceed with this ticket's original design —
the capability-broker MCP server with keys embedded in the
subagent's .md body.

The two remaining empirical prerequisites for THIS ticket's
design to work (replacing the old single prerequisite):

  1. Is mechanism Agent interface contract: parent-facing skill, schema enforcement, and discovery #4 viable? — can a string embedded in
    the subagent's own .md body stay opaque to the parent?
    If not, the broker's key is leakable and the design
    collapses.
  2. Does plugin-bundled MCP invocation work reliably from
    a subagent?
    — the upstream bugs listed above affect
    inline MCP in subagent frontmatter; the community
    workaround is "move the MCP server to plugin or global
    scope." Empirical confirmation still pending.

Both are being investigated in the same experimental
workstream that produced the mechanism-#3 verdict. This
ticket remains BLOCKED on those two sub-questions, but no
longer blocked on the (now-closed) primary prerequisite.

Prerequisite: verify asymmetric built-in-tool grants first

Before proceeding with the workaround-flavored MCP server in
this ticket, one of the alternative mechanisms — asymmetric
grants on built-in tools (Read / Bash), where the subagent
has access to sensitive content that the parent does not

should be empirically verified. That mechanism, if it works,
would make the MCP server in this ticket unnecessary, because
the standard built-in-tool permission surface plus subagent
context isolation would already solve the problem without any
plugin-level machinery.

That verification is being conducted separately by repo
contributors in an experimental research workstream. Any
contributor who wants visibility into that work can reach out
to a maintainer; the decision criteria and the verdict will be
documented back into this ticket (and a successor ticket if
the verdict points at a different design) before
implementation begins.

Exit criteria for the prerequisite

The separate investigation is complete for the purposes of
this ticket when all of the following are recorded here or in
a linked doc:

  • Paired positive/negative test evidence showing, for built-in
    Read (and ideally Bash) tools, whether the subagent can
    access a path the parent cannot, under configuration applied
    via frontmatter or project settings — the key question being
    whether the asymmetry is structurally enforceable.
  • A clear verdict: the asymmetric-grant mechanism works
    reliably / works with caveats / does not work.
  • A stated next action for this ticket based on that verdict:
    close as not-needed, proceed as specified, or revise the
    design.

Even if the prerequisite confirms #3 works, it isn't an automatic choice

Mechanism #3 — asymmetric grants on a built-in tool like
Read — has an inherent cost that the prerequisite test
will not by itself resolve: it requires embedding a
concrete filesystem path (or glob) in the agent definition
or in project/plugin settings
. The subagent's tools:
frontmatter entry, or the settings' permissions.allow
pattern, has to name where the sensitive content lives.

That has real downsides independent of whether the
asymmetric-grant mechanism works:

  • Portability. Users on different OSes, different XDG
    overrides, or different personal conventions have content
    in different places. A hard-coded path means either
    shipping multiple variants, accepting fragility, or forcing
    every user into one specific location.
  • Permission-surface shape. A path-specific Read(...)
    grant exposes exactly one specific filesystem pattern to a
    generic Read tool. Security review has to reason about
    both what Read can do and what paths it is allowed on. A
    custom tool with a tight interface (takes an opaque key,
    only loads .md files, only from a map the user controls)
    is easier to reason about: one MCP-tool-specific permission
    that names a single well-scoped tool.
  • Install UX. A custom MCP tool gets one permission
    approval per install (or zero if the plugin marketplace
    handles it). Per-path Read grants either require manual
    edits to settings or a plugin-supplied settings delta that
    has to be correct for every user's file layout.
  • Evolution. If the set of loadable artifacts grows or
    changes, the custom-tool side only requires editing a
    user-land config file. The path-grant side requires
    editing agent definitions or settings — published and
    versioned artifacts.

So even in the branch where the prerequisite verifies #3
works structurally, the verdict for THIS ticket should be a
two-part question, not a one-part question:

  1. Does the asymmetric-grant mechanism work reliably?
  2. Is its path-hardcoding shape acceptable for the use
    case at hand, given the portability and permission-surface
    tradeoffs above, OR is the custom-tool surface proposed in
    this ticket preferable on those grounds independent of the
    upstream-bug-workaround motivation?

That second question deserves a deliberate answer documented
alongside the first. It is entirely possible that #3 works
and we still build this MCP server because the permission
surface and portability are strictly better.

Possible outcomes

Work on this ticket should NOT begin until the prerequisite
is resolved AND the two-part question above has a
deliberate, documented answer.

"How else might we do this" — preserved-alternatives capture

To avoid re-deriving the design space from scratch in a future
session, CONTRIBUTING.md (or a linked sibling .md, e.g.
docs/context-mcp-design.md) should capture the alternative
mechanisms that WOULD become viable or attractive if one or
more of the upstream issues above close, along with what
enables each and what the shape of the alternative would be.

These are NOT decided preferences. They are notes-to-self:
research was done once; capturing it at head means a future
session reconsidering the design can start from the saved
sketch rather than re-evaluating from zero.

Each captured alternative should include:

  • Short name for the mechanism.
  • Which upstream issue(s) closing with a fix would unlock or
    make it attractive (cross-reference the issue list above).
  • One-paragraph sketch of the shape — what the configuration
    would look like, where the sensitive content lives, how the
    parent is structurally excluded.
  • One-paragraph honest account of why it is NOT the chosen
    path today — upstream blocker(s), tradeoff against the
    current design, or both.
  • Whether adopting it would deprecate this MCP server
    entirely, replace part of it, or complement it.

Suggested mechanisms to capture at minimum (expand as
appropriate during implementation):

  1. Inline MCP server declared in the subagent's
    frontmatter.
    Sensitive content accessed via a tool whose
    MCP server is declared mcpServers: on the subagent's
    .md. Parent's tool catalog structurally excludes it.
    Unlocked by: #13898 and/or #25200 closing with a fix.
  2. Project-scope MCP + per-agent tools: whitelist. MCP
    server declared once at project or plugin scope; each
    agent's frontmatter whitelists which of its tools to
    surface. Parent's whitelist excludes the sensitive tool;
    subagent's includes it. Unlocked by: #25200 and/or #33689
    closing with a fix.
  3. Asymmetric built-in-tool grants (Read/Bash). No MCP at
    all. Subagent granted Read on the sensitive file via its
    frontmatter or project settings; parent explicitly denied
    via permissions.deny or by tool-restriction frontmatter.
    Subagent context isolation prevents raw content from
    bleeding back. Unlocked by: empirical verification of
    parent/subagent asymmetry for built-in-tool grants (does
    not depend on any of the upstream MCP bugs being fixed —
    currently blocked only by untested-ness).
  4. Hook-based caller-identity enforcement. A
    PreToolUse hook rejects tool invocations whose
    parent_tool_use_id indicates parent scope, allowing only
    subagent-originated calls. Layered on top of any of the
    mechanisms above as defense-in-depth. Unlocked by:
    empirical confirmation that hook payloads expose
    parent_tool_use_id and that hooks can structurally abort
    tool calls based on it.

The captured section should make clear that any future move
off the current design is driven by evidence (upstream fix
landed, empirical verification completed, etc.) and that the
current design stands until that evidence arrives.

Deliverables

  • New MCP server source under this plugin (e.g.
    server.py at plugin root, if claude-coding adopts the same
    layout as the scaffold plugin).
  • .mcp.json entry wiring the server via
    ${CLAUDE_PLUGIN_ROOT} + ${CLAUDE_PLUGIN_DATA}
    PYTHONPATH pattern.
  • requirements.txt listing the mcp package (and
    anything else the server needs).
  • hooks/hooks.json SessionStart hook using the
    requirements.txt-diff install-trigger pattern per
    plugin-patterns.md.
    Do not use an in-package version-string compare.
  • Config loader in the server that reads the user's
    context-map.toml (or chosen format/name) from the chosen
    XDG location.
  • .md-only format gate in the load_context tool
    implementation.
  • Unit / integration tests covering: valid key + valid
    path → returns content; missing file → structured error;
    unknown key → structured error; non-.md target →
    structured error; missing config → structured error.
  • Update the relevant subagent's .md body to embed the
    key it should call with (no key in frontmatter; no key in
    project settings).
  • Resolve the ./agent script question (deprecate / extend
    / leave) per the section above. Record decision and rationale
    in CONTRIBUTING.md and update README.md install
    instructions to match.
  • Update CONTRIBUTING.md with a section explaining the
    design and the YAGNI + workaround framing above, so the head
    revision documents why this shape was chosen over the more
    obvious alternatives. Include a short pointer (not a
    restatement) to plugin-patterns.md for the install-trigger
    pattern and antipattern rationale — one canonical source,
    one pointer.

Out of scope

  • A general-purpose capability-broker design with namespaces,
    audit logs, multi-tenancy, etc.
  • An admin / editor CLI for maintaining the config map. Users
    edit the TOML by hand for now.
  • Integration with any secrets-management product.
  • Revisiting the design if the upstream subagent-inline-MCP
    bugs close with fixes that eliminate the motivation — that's
    a future ticket, not this one.

Verification

After the work lands, a reader of this plugin's head revision
should be able to answer, from CONTRIBUTING.md and the
server source alone:

  • What does the load_context tool do and what keys are
    valid?
  • Where does the key→path mapping live?
  • Why does this plugin carry a load_context tool at all,
    and why not something more general?
  • What upstream Claude Code limitations is this partly
    working around, and under what conditions would we
    reconsider keeping it?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions