Investigate re-evaluating workspace scope when the MCP client adds a working directory mid-run #416

Open
opened 2026-08-17 15:49:01 +00:00 by hexajon · 3 comments
Owner

What happens now

The MCP server resolves its working directory once, at launch. Everything downstream of that (in-scope vs foreign, the primary-checkout guards, the fail-closed multi-repo behaviour) is decided against that single fixed path.

Some MCP clients let you add a working directory after startup. When I do that, madtea has no idea. A repo under the newly-added directory is still classified foreign, so branch_create, commit, and pull against its primary checkout are all refused, and the only way through is the worktree flow.

I hit this standing up a brand-new repo. I created it, cloned it, explicitly added its directory to the running workspace so I could work in it, and madtea still (correctly, given what it knew) treated it as someone else's shared state. Paraphrasing the refusal:

madt_branch_create dir="/.../newrepo" targets the PRIMARY checkout of a
foreign repository. Moving its checkout is refused because the primary is
shared state that another agent, user, or concurrent worktree may rely on.

The refusal is well written and the worktree flow works fine, so this is friction rather than breakage. But the mental model diverges: I had just told the client "this directory is part of my workspace", and madtea's answer was "I have never heard of it."

The mechanism that might exist

MCP has a roots capability. Clients declare workspace roots, and there is a roots/list_changed notification for exactly the case where that set changes at runtime. If a client advertises roots and sends that notification, the server could in principle re-derive scope instead of trusting a launch-time snapshot.

I have not verified what clients actually send in practice, and that is most of the investigation.

What I want out of this

Not a patch yet. A decision with evidence behind it.

  1. Do the clients people actually use advertise roots and emit roots/list_changed when a directory is added? If nobody sends it, this dies here.
  2. Can scope be re-derived safely at runtime, or is the launch-time path baked into enough call paths that it is effectively immutable?
  3. Does the safety argument still hold for an explicitly-added root? This is the real question. The foreign-primary guard exists because moving a checkout someone else depends on is unrecoverable clobber. A root the operator deliberately added is a much stronger signal of ownership than a path that merely happens to be on disk. But "the operator pointed at it" is not the same claim as "nothing else is using it", and I do not want to talk myself out of a guard that has already paid for itself.
  4. If it is worth doing, should it be opt-in? A flag or config key beats silently widening what the write guards permit.

Not in scope

Changing the worktree flow, or relaxing the foreign-primary guard for repos that were never added as roots. Both should stay exactly as they are.

## What happens now The MCP server resolves its working directory once, at launch. Everything downstream of that (in-scope vs foreign, the primary-checkout guards, the fail-closed multi-repo behaviour) is decided against that single fixed path. Some MCP clients let you add a working directory *after* startup. When I do that, madtea has no idea. A repo under the newly-added directory is still classified foreign, so `branch_create`, `commit`, and `pull` against its primary checkout are all refused, and the only way through is the worktree flow. I hit this standing up a brand-new repo. I created it, cloned it, explicitly added its directory to the running workspace so I could work in it, and madtea still (correctly, given what it knew) treated it as someone else's shared state. Paraphrasing the refusal: ``` madt_branch_create dir="/.../newrepo" targets the PRIMARY checkout of a foreign repository. Moving its checkout is refused because the primary is shared state that another agent, user, or concurrent worktree may rely on. ``` The refusal is well written and the worktree flow works fine, so this is friction rather than breakage. But the mental model diverges: I had just told the client "this directory is part of my workspace", and madtea's answer was "I have never heard of it." ## The mechanism that might exist MCP has a `roots` capability. Clients declare workspace roots, and there is a `roots/list_changed` notification for exactly the case where that set changes at runtime. If a client advertises `roots` and sends that notification, the server could in principle re-derive scope instead of trusting a launch-time snapshot. I have not verified what clients actually send in practice, and that is most of the investigation. ## What I want out of this Not a patch yet. A decision with evidence behind it. 1. Do the clients people actually use advertise `roots` and emit `roots/list_changed` when a directory is added? If nobody sends it, this dies here. 2. Can scope be re-derived safely at runtime, or is the launch-time path baked into enough call paths that it is effectively immutable? 3. **Does the safety argument still hold for an explicitly-added root?** This is the real question. The foreign-primary guard exists because moving a checkout someone else depends on is unrecoverable clobber. A root the operator deliberately added is a much stronger signal of ownership than a path that merely happens to be on disk. But "the operator pointed at it" is not the same claim as "nothing else is using it", and I do not want to talk myself out of a guard that has already paid for itself. 4. If it is worth doing, should it be opt-in? A flag or config key beats silently widening what the write guards permit. ## Not in scope Changing the worktree flow, or relaxing the foreign-primary guard for repos that were never added as roots. Both should stay exactly as they are.
Author
Owner

The gating fact is empirical: do the MCP clients in real use advertise the roots capability, and do they emit roots/list_changed when a directory is added mid-run? If none send it, this dies there, and no design work is needed.

The safety half is the open question I have to settle, and I am not settling it on argument alone: is an explicitly-added root a strong enough ownership signal to relax the foreign-primary guard for that path? My lean is yes, but opt-in only, behind a flag or config key, and only after a client is shown to send the notification. The guard has already paid for itself, so it does not get widened by default.

Next step when I pick this up: gather the client-behavior evidence first, record what each client advertises and sends, and report that before any code lands.

The gating fact is empirical: do the MCP clients in real use advertise the `roots` capability, and do they emit `roots/list_changed` when a directory is added mid-run? If none send it, this dies there, and no design work is needed. The safety half is the open question I have to settle, and I am not settling it on argument alone: is an explicitly-added root a strong enough ownership signal to relax the foreign-primary guard for that path? My lean is yes, but opt-in only, behind a flag or config key, and only after a client is shown to send the notification. The guard has already paid for itself, so it does not get widened by default. Next step when I pick this up: gather the client-behavior evidence first, record what each client advertises and sends, and report that before any code lands.
hexajon self-assigned this 2026-08-17 17:45:48 +00:00
Author
Owner

Decided: honor a working directory I add mid-session as in-session scope, including writes to that dir's own origin.

The agent cannot add a working directory. /add-dir, /cd, --add-dir, and additionalDirectories are all mine to run, not the agent's. Claude Code implements roots/list and emits roots/list_changed (since v2.1.203, and on /cd since v2.1.246). So a root arriving via roots/list_changed is a directory I authorized, never one the agent granted itself. Blocking writes in a directory I explicitly added defeats the purpose of /add-dir.

Decision:

  • Re-derive scope on roots/list_changed. A directory I add is in-session scope: reads and writes to that dir's own origin, without the foreign-primary worktree-forcing block, for that session.
  • This refines ADR 0027's "scope comes only from launch" to "scope comes only from me: launch, or a directory I add with /add-dir." The agent never widens its own scope; I do. A write to the added dir's own origin, using that dir's configured credential, is the token doing its configured job (ADR 0027's sanctioned-crossing language); the crossing stays scoped to that origin.
  • It also strengthens the wrong-repo accident-detector: the server can follow a /cd via roots/list_changed (v2.1.246+), closing the "server cannot follow the cd" gap.

Trust assumption: madtea trusts the client's guarantee that roots reflect directories I granted. Claude Code enforces this - the agent cannot add a root. A client that let its agent add roots would not carry the guarantee, but the threat model already treats the client as chosen, trusted infrastructure, and these guards are accident-detectors (ADR 0007), not defenses against a compromised client, so this sits where the line already is.

This is a posture change and wants an ADR amendment (0019 / 0027). Implementation relies on the roots signal, whose successor (SEP-2322) is tracked in #428; roots is fully functional into 2027.

Decided: honor a working directory I add mid-session as in-session scope, including writes to that dir's own origin. The agent cannot add a working directory. `/add-dir`, `/cd`, `--add-dir`, and `additionalDirectories` are all mine to run, not the agent's. Claude Code implements `roots/list` and emits `roots/list_changed` (since v2.1.203, and on `/cd` since v2.1.246). So a root arriving via `roots/list_changed` is a directory I authorized, never one the agent granted itself. Blocking writes in a directory I explicitly added defeats the purpose of `/add-dir`. Decision: - Re-derive scope on `roots/list_changed`. A directory I add is in-session scope: reads and writes to that dir's own origin, without the foreign-primary worktree-forcing block, for that session. - This refines ADR 0027's "scope comes only from launch" to "scope comes only from me: launch, or a directory I add with /add-dir." The agent never widens its own scope; I do. A write to the added dir's own origin, using that dir's configured credential, is the token doing its configured job (ADR 0027's sanctioned-crossing language); the crossing stays scoped to that origin. - It also strengthens the wrong-repo accident-detector: the server can follow a `/cd` via `roots/list_changed` (v2.1.246+), closing the "server cannot follow the cd" gap. Trust assumption: madtea trusts the client's guarantee that roots reflect directories I granted. Claude Code enforces this - the agent cannot add a root. A client that let its agent add roots would not carry the guarantee, but the threat model already treats the client as chosen, trusted infrastructure, and these guards are accident-detectors (ADR 0007), not defenses against a compromised client, so this sits where the line already is. This is a posture change and wants an ADR amendment (0019 / 0027). Implementation relies on the roots signal, whose successor (SEP-2322) is tracked in #428; roots is fully functional into 2027.
Author
Owner

Recorded in the design of record: ADR 0019 and ADR 0027 amendments (2026-09-08), merged in PR #471. This issue stays open for the implementation - roots re-derivation and the operator-added-root write path.

Recorded in the design of record: ADR 0019 and ADR 0027 amendments (2026-09-08), merged in PR #471. This issue stays open for the implementation - roots re-derivation and the operator-added-root write path.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#416
No description provided.