The Task spawn payload cannot carry a worktree destination. Fail-closed preToolUse hooks that require tool_input.isolation, worktree_path, or cwd therefore deny every implement-class spawn.
Published Task schema fields are only things like description, prompt, subagent_type, model, run_in_background. There is no isolation, worktree_path, or cwd. Extra keys sent on the Task call are stripped before the hook sees tool_input. best-of-n-runner fails the same way.
This blocks worktree-isolated agent dispatch (Deft Directive and any host that refuses inherited-cwd mutation).
Steps to Reproduce
Install a Cursor preToolUse hook on Task|SubagentStart|spawn_subagent|start_agent|CreateAgent that requires tool_input.isolation=worktree or worktree_path/cwd pointing at a linked git worktree.
Create a linked worktree.
Ask the Agent to spawn a mutating subagent with isolation: “worktree” and worktree_path / cwd set to that path.
Observe the hook deny: destination missing on the spawn payload.
Expected Behavior
Task/spawn schema includes isolation, worktree_path, and/or cwd.
Those fields reach PreToolUse tool_input (do not strip destination keys before hooks run).
Optional: isolation=worktree with no path auto-provisions a linked worktree.
Operating System
MacOS
Version Information
Cursor Agent, 2026-09-08. Reproduced with Deft @deftai/[email protected]. Hook error: Directive denied implement-class spawn: no worktree destination on the spawn payload (tool_input.isolation=worktree, worktree_path, or cwd).
Thanks for the feedback! There isn’t a per-spawn worktree destination on the Task call today (no isolation, worktree_path or cwd field), so a hook has nothing to verify at spawn time. The built-in best-of-n-runner gets its worktree by creating it as its first step, which is why it hits the same denial. I’ve moved this over to Feature Requests.
That said, there’s another option that may fit Directive’s model well: cloud subagents.
From a local agent, you can spawn a subagent in the cloud. Use the /in-cloud command, or simply ask the agent to run the subagent in the cloud. It gets its own VM, its own clone, and its own branch off the remote base branch, so nothing it does can touch the parent’s checkout.
The useful part for a fail-closed hook: the Task call’s environment field ("local" or "cloud") is included in tool_input for preToolUse. That means Directive can allow implement-class spawns when environment is "cloud", and can set it via updated_input if the model leaves it out.
Two caveats to keep in mind:
The cloud subagent works from the pushed branch, so uncommitted local changes aren’t visible to it.
When it finishes, you merge or check out its branch rather than finding edits in your working tree.
Thanks Colin — this matches what we see, and sending this over to Feature Requests is appreciated.
Confirming the platform facts you stated, so we’re not talking past each other:
Task has no per-spawn isolation, worktree_path, or cwd today.
Extra keys on the call are stripped before preToolUse, so a fail-closed hook has nothing to verify.
best-of-n-runner creating a worktree as its first step is too late: admission already happened. That’s why it dies on the same deny.
environment ("local" | "cloud") does reach tool_input. That’s the only spawn-time isolation signal hooks can see right now.
Cloud subagents are an option, and we’ll use them where they fit. However, they don’t replace a local worktree destination on Task.
Why environment: "cloud" is not the same gate
A fail-closed hook’s job is to refuse an implement-class spawn unless the child is already bound to a destination that cannot mutate the parent checkout. Cloud satisfies “not the parent cwd” in a coarse way: different VM, different clone, different branch. That is useful, and we can special-case environment === "cloud" in the hook.
But it does not satisfy the local isolation contract we actually run:
Spawn-time proof, not post-admit hope. We need the destination on the payload before the child is allowed to start. “The runner will git worktree add after it begins” is exactly the race the deny is for — the child is already a mutating agent on the inherited checkout.
A specific linked worktree, not “somewhere else.” Local swarms bind one child to one path (e.g. .deft-scratch/worktrees/<story-id>), one branch, one occupancy lease, one file-scope fence, one heartbeat dir. Cloud gives a fresh remote clone. That’s isolation, but it’s a different topology: no shared local lease, no parent-adjacent worktree the monitor can cd into, no uncommitted parent state, no local Docker/Postgres/secrets.
Push-first is a different workflow. Cloud only sees what’s on the remote. Our implement path is often: create worktree → write xBRIEF → implement → preflight → then push. Requiring a push before the child can exist means either the parent implements (the thing spawn isolation is supposed to prevent) or we invent a hollow branch just so the cloud leaf has a tip. It’s a workaround, but is not really a substitute.
Allowing environment: "cloud" is a policy carve-out, not verification. The hook can see the string "cloud". It cannot see which worktree, cannot bind occupancy, cannot prove the child won’t come back and touch the parent tree. We’ll take the carve-out. But it’s still a hole in the local model.
So: yes, we’ll add “allow implement-class spawn when environment === "cloud"” (and we may updated_input that if the model omits it).