Skip to content
Troubleshooting All of Documentation

Workspace, shell and tools

A refused path, a denied tool, the approval round trip, and tool output that is capped or empty.

19 min readUpdated 13 Sept 2026Reviewed 12 Sept 2026Published 12 Sept 2026/docs/troubleshooting/workspace-and-tools

This page covers everything that happens between an employee and the machine: a path the workspace resolver refused, a tool the role was not granted, an approval round trip that is waiting or was declined, and output that came back shorter than you expected. For the question of what these tools can reach in the first place, read Known gaps.

What these failures have in common

Tool calls do not crash turns. Every tool returns a result object, and a tool that throws is caught and converted into an error result, because a turn that dies because a file was missing is worse than a turn that is told the file was missing. That design is why almost every symptom on this page appears as text inside a tool result rather than as an exception, and why the place to look is the tool call record rather than the run.

Two things are worth checking before the individual symptoms. First, which tool actually ran: the tool call record carries the name, the arguments, the status, a short preview and the duration, and the status vocabulary is small — ok, error, denied or running. Second, which workspace root it ran against, because that is the run's own project directory rather than the office default, and it is the string every refusal quotes.

One fact underneath all of this is worth stating plainly before any symptom does. The workspace boundary protects one project from another project; it does not protect the machine from the operator. Fifteen tools ship, eight of them take a path and resolve it through the workspace guard (the two listing tools, the three readers, the two writers and the patch tool), run_shell is confined by nothing except its working directory, git is not path-confined either, and an MCP or plugin tool is not confined by the host at all. The details are in Known gaps.

Refusing path ...: it escapes the workspace root ...

What you see. A tool result containing Refusing path "../outside.txt": it escapes the workspace root "...", with the root printed as the run's own project directory. The tool call's status is error.

What it means

Eight of the fifteen tools resolve every path argument through one shared guard before touching anything: the two listing tools, the three readers, the two writers and the patch tool. The guard resolves the candidate against the run's workspace root, rejects parent-directory escapes and drive-relative forms outright, normalises it, compares the result with the root, and then resolves the real path and refuses any symlink, junction or mount point between the root and the target. Anything that lands outside — or that reaches outside through a link — is refused with this message.

Note which root is quoted. It is the run's workspacePath, not the installation's default workspace, so two runs in two projects refuse each other's paths even when both are legitimate. A refusal is therefore often correct and the interesting question is which project the run belongs to.

What to check

  1. Read the printed root. Compare it with the floor the run was submitted on; a run on the wrong floor produces refusals that look arbitrary.
  2. Check the path for a parent-directory segment. Normalisation happens before the comparison, so a path that merely looks contained can still escape after it.
  3. Check for an absolute path. Absolute paths are allowed only when they resolve inside the root; a path outside it is refused rather than rewritten.
  4. Check the run's project was created where you think it was. New projects live under the configured workspaces root unless an external path was given, and external paths are refused outright when that permission has been turned off.
  5. Check the file is not simply missing. A missing file inside the workspace produces File does not exist: ..., not a refusal; the two messages mean different things.

Refusing drive-relative path "C:foo"

What you see. Refusing drive-relative path "C:foo": it is not confined to the workspace.

What it means

On Windows, a path of the form drive-letter-colon-name is resolved by the operating system against the current directory on that drive, not against the workspace root. It is a real escape route that looks like a relative path, so the guard rejects the shape before resolving anything. This is a deliberate refusal of a path form rather than of a destination.

What to check

  1. Rewrite the path relative to the workspace root instead of naming a drive, for example a plain relative path with forward slashes.
  2. Check where the path came from. A drive-relative form is usually assembled by a model from a Windows path it half-remembered, so the useful fix is in the prompt or the task text rather than in the tool.

A tool reached outside the workspace and nothing stopped it

What you see. Evidence that a command or a tool touched a file outside the run's project — and no refusal in the tool result.

What it means

This is not a bug, and it is the single most misunderstood part of the system. Eight of the fifteen built-in tools use the workspace guard — the ones that take a path — and the rest are not equivalent to each other:

  • think and todo_write touch no path at all, so there is nothing to confine: one is a scratchpad, the other writes the run's plan.
  • web_search and web_fetch are guarded by address rather than by path: a loopback, private, link-local, CGNAT or cloud-metadata address is refused on every redirect hop. That is a real guard, and it does not defend against DNS rebinding.
  • run_shell is confined only by its working directory. The command is spawned through the platform shell with the workspace root as the working directory, and the shell text is never resolved against the workspace, so arbitrary shell text can read or write anywhere the process can reach. Its child environment is stripped of credential-named variables, which removes the easy path and nothing more.
  • git is not path-confined either: it runs as an argument vector against the repository, so it splits by what a command can do rather than by where it points.
  • A tool contributed by a plugin is not confined by the host at all: the host hands the plugin the workspace root as information, and forwards whatever the plugin returns. An MCP server's tools are likewise unconfined, which is why they are granted to nobody by default.

Two consequences follow and both matter. The approval round trip is the only control on the shell tool — and on the writing half of git, and on a vendor delegation — and the auto-approve switch skips all three. And because a code plugin already runs in the orchestrator's process with the orchestrator's authority, this is consistent rather than surprising — but it does mean that "one choke point" describes the built-in set and not the tool list an installation actually has once a plugin or an MCP server is connected.

What to check

  1. Identify the tool from the tool call record. The distinction is entirely by tool name, not by outcome.
  2. Read the confinement section of Known gaps rather than relying on a summary; the four unconfined cases have four different explanations.
  3. Decide the boundary at the machine, not at the office, if you need one. The office has no sandbox: run it under an account with the access you are willing to grant it.
  4. Keep the approval round trip on unless the installation is genuinely unattended, and read Building a plugin before granting a plugin tool access to anything you care about.

This is a documented limit, not a defect. The gap for the shell tool and for plugin-provided tools is listed with the other limits in Known gaps, and no automated check in the project guards it. Do not read the workspace resolver as a sandbox around the process.

The human declined to run this shell command

What you see. A tool result reading The human declined to run this shell command. Do not retry the same command; ask for clarification or propose a non-shell alternative. The tool status is error, not denied.

What it means

The shell tool is the only tool that asks before acting. It raises an approval carrying the command's first line as the summary and the full command plus the working directory as the detail, and it waits. The text you are seeing is the tool telling the model that the answer was no, in terms designed to stop it retrying the same command.

A declined approval and a timed-out approval produce the same result. A timeout resolves the approval as refused, with a log line at scope approvals recording the fact and the configured wait, because a pending approval that nothing can answer must not wedge a run forever.

What to check

  1. Find the approval in the console. Its summary is the command's first line and its detail is the exact command with the working directory, which is what a person needs in order to answer.
  2. Check whether it timed out rather than being refused. The approvals scope logs the timeout and the wait; the default wait is ten minutes.
  3. Check whether anybody is watching. An unattended installation will decline every shell command by timing out, which looks like a model that cannot use the shell.
  4. Read the model's next action before changing anything. The tool text tells the model to propose an alternative, so a turn that then tries a non-shell route is working correctly.

The fix. Answer the approval promptly if a person is present. For an unattended installation, decide deliberately between two options: turn shell auto-approval on and accept that the most dangerous tool is no longer supervised, or grant roles without the shell tool so the request never happens. The approval timeout is an installation setting, so it belongs in the console rather than in the file.

Nobody was asked, and shell commands ran anyway

What you see. Shell commands execute with no approval request in the console, and the boot log carries DEV3D_AUTO_APPROVE_SHELL is on: employees may run shell commands without asking.

What it means

Shell auto-approval removes the only gate in front of the only unconfined built-in tool. The switch is a literal string comparison, so only the exact value true enables it, and it is copied into the installation settings on the first boot — after which the console, not the file, is the authority.

What to check

  1. Check where the value came from. If it was set in the environment for a one-off unattended run, the saved setting may still have it on afterwards.
  2. Check which roles hold the shell tool. A role without the shell tool cannot use the switch at all; the grants are per role, not per installation.
  3. Check whether the run is truly unattended. If a person is at the console, an approval costs seconds and buys the only control the design has.

A run sits in awaiting-approval

What you see. The run's status is awaiting-approval, the employee who raised the request shows as blocked or as needing approval, and nothing moves until somebody answers.

What it means

While an approval is pending, the run is genuinely waiting on a person rather than on a model. The employee's status becomes blocked with the approval's summary as its activity, the run's status becomes awaiting-approval, and a run update is emitted so every console agrees. When the approval is answered the employee goes back to working and the run returns to running.

Only a tool approval sets that run status. The soft-spend gate asks a human too, but it does not set the status — it simply waits between stages — which is why a spend question can sit on a run that still looks as though it is running. See Budget, cost and stuck runs for that one.

What to check

  1. Count pending approvals at the health endpoint. pendingApprovals is the cheapest way to confirm a run is waiting on a person rather than on a provider.
  2. Read the approval's detail, not its summary. The summary is the command's first line, truncated; the detail is the exact command and the working directory it will run in.
  3. Answer it through the approval command on the socket. There is no HTTP route for it; see Wire protocol.
  4. Check the wait you configured. The approval timeout floors at one second and defaults to ten minutes, and a timeout is a refusal rather than an error.
  5. Check the run has not already moved on. The health endpoint's activeRuns and a fresh run read answer whether the run is still alive at all.

refused: tool is not granted to a role

What you see. A tool call with status denied and a preview reading refused: run_shell is not granted to frontend-dev-1, or a preview reading unknown tool some_name.

What it means

Grant enforcement happens in the engine rather than in the tool registry. The registry knows what tools exist; the org chart decides who may hold them. A model that calls something outside its grant is told which tools it does have and asked to continue without it, which is why the turn carries on rather than failing.

unknown tool is a different fact with a similar shape: the name is not in the registry at all. That happens when a model invents a plausible tool name, and it also happens when a plugin declares a tool name in its manifest and then never registers it, in which case the plugin's contributions and the registry disagree.

What to check

  1. Read the role's grant list. The tools an employee holds are visible on that employee, and grants are editable from the org chart.
  2. Check the shipped defaults before assuming a bug. Roles are seeded from three sets: read-only reconnaissance, read plus document writing, and the developer set that adds search, editing and the shell.
  3. For an unknown name, check the plugin that claims it. The registry name and the manifest's declared tool names are two separate facts; see Plugins and skills.
  4. Check the tool name is not namespaced. A plugin tool is registered under a prefixed name derived from the plugin's id, so the name in the manifest is not the name the model must call.

A file tool refuses to read or edit a file

What you see. One of a small family of results: File does not exist: ..., "..." is a directory; use list_dir to see its contents., "..." is 2048576 bytes; refusing to read files larger than 1 MB., "..." looks like a binary file; refusing to read it., or edit_file: oldString was not found in ... It must match the file exactly (no partial lines).

What it means

These are caps rather than failures, and each one exists so that a single careless call cannot fill a model's context with something useless. Reading is capped by size and refuses anything containing a null byte in its first few kilobytes, which is how binary detection works. Listing is capped by entry count and depth and skips the two directories nobody wants in a result. Searching stops at a fixed number of matches and skips files above a size threshold. The editor requires its old text to appear exactly, and refuses an ambiguous match rather than guessing which occurrence was meant.

Every one of these messages is written for the model, not for you, so each says what to do instead. That is also why the limit is a string in a tool result rather than a thrown error: the turn keeps going with the information.

What to check

  1. Read the message for the limit it names. Size, line count, entry count, match count and binary refusal each name their own threshold.
  2. For an edit that did not match, check whitespace and line endings. The comparison is literal, so a trailing space or a carriage return that the model did not reproduce is enough to fail it.
  3. For an ambiguous edit, give more context or allow all occurrences deliberately. The tool offers both routes in its own message, and replacing one of several identical blocks by accident is the failure the cap prevents.
  4. For a large file, read it in ranges. The read tool takes a start and end line, and its own result says which lines it returned of how many.
  5. Check the path is inside the run's workspace before concluding the file is missing; a refusal is a different message.

Command timed out after 30000 ms and was killed

What you see. Command timed out after 30000 ms and was killed. followed by whatever output the command produced before it was killed, or (no output). Related results read exit code: 1, (output truncated) or (no output).

What it means

The shell tool runs the command through the platform shell with the workspace root as the working directory, and kills it if it is still running at the timeout. The default is thirty seconds and the hard maximum is two minutes; a request for longer is clamped rather than refused. Output is capped by character count and marked when it is cut, and a command that produces nothing at all says so explicitly rather than returning an empty string that reads like a bug.

Two details explain most surprises here. Standard error is folded into the result under a labelled section, so a command that failed with an empty standard output still has its explanation in the result. And a nonzero exit code is not an error status for the tool: the tool reports the exit code, and the status line is what you actually read.

What to check

  1. Check the exit code in the result. A command that exits nonzero without producing output is reported as a failed command with its code, not as a crash.
  2. Check the timeout you asked for. A per-call timeout is clamped between one millisecond and two minutes, so a long build should be re-run with an explicit higher timeout or split into steps.
  3. Check the working directory assumption. The command runs in the run's project directory, so a relative path in the command is relative to that, not to the checkout.
  4. Check for truncation. Output is cut at a fixed character count and marked; a result that ends mid-line without the marker was the command's own output.
  5. Check shell substitution. On Windows the command runs through the command interpreter, so its own variable syntax applies rather than a POSIX shell's.

Stopped after 8 tool round trips without a final answer

What you see. A turn whose error field reads Stopped after 8 tool round trips without a final answer., or The model hit its output limit mid-turn; the reported work product is incomplete.

What it means

Both messages come from the turn loop's own limits. A turn stops after a fixed number of tool round trips, because a model that keeps calling tools without concluding would otherwise run up a bill indefinitely. Separately, a model that hits its per-turn output cap mid-answer records that the work product is incomplete.

This case used to have a trap worth knowing, and the trap has been closed. A turn that hit its output cap mid-turn recorded an error saying the work product was incomplete while its status stayed done, no error event was emitted, and the employee returned to idle — so a partial work product looked clean everywhere except the one field carrying the message. Both partial failures now settle as failed turns and reach the wire. If you are reading history recorded before that change, the old rows still look clean, which is worth remembering when comparing runs across the boundary.

What to check

  1. Read the turn's error field, not just its status. This is the single most important habit for automating on run results.
  2. Count the tool calls on the turn. A turn that hit the round-trip limit is doing work that may belong in a longer pipeline rather than in one turn.
  3. Check the output cap for the role. Policies carry a per-turn output cap, and the shipped default is not universal across roles.
  4. Check whether a retry was expected. The turn prompt supports a correction note for a retry, but no caller passes one, so a failed turn is not retried with an explanation. See Known gaps.

A web tool returns nothing usable

What you see. Only http:// and https:// URLs are allowed (got ...)., Web search was unavailable (DuckDuckGo returned HTTP 403)., Web search was unavailable: the search engine returned no parseable results., or Network request to ... failed: ... Check the URL and connectivity, then try again.

What it means

The two web tools are the least robust in the office and the documentation should say so plainly. The fetch tool is ordinary HTTP with a timeout: it strips scripts, styles and tags, collapses what remains into text, and caps the result. The search tool reads a search engine's HTML results page and parses the markup, which means it breaks when that markup changes or when the request is refused — and its failure mode is a clear message rather than an empty result, which is the honest way to fail.

Both cap their output and both time out. Neither is a confinement mechanism: accepting only http and https is a protocol limit, not a workspace boundary.

What to check

  1. Read the message for the status. An HTTP status means the request reached the service; a network error means it did not.
  2. For a fetch, check the URL itself. The tool reports the final URL after redirects, and a status line before the body, so a soft 404 is visible in the result rather than hidden.
  3. For a search, treat repeated parse failures as a parser problem, not a query problem. Rewording the query will not fix markup the parser does not recognise.
  4. Check the network path. A sandbox or proxy that blocks outbound requests produces a network error for both tools and nothing else in the office.
  5. Expect a truthful gap rather than a fabricated answer. The tool tells the model to say it could not reach the network instead of inventing sources; a turn that does exactly that is working correctly.

The tool "X" crashed

What you see. A tool result reading The tool "..." crashed: ..., with the tool call's status error and the message after the colon taken from the thrown error.

What it means

Tools are supposed to return a result rather than throw, and the filesystem tools wrap their own bodies so that a thrown path error becomes an ordinary error result. This message exists for the case where something throws anyway — an unexpected filesystem condition, or a plugin tool that did not contain its own failure. The engine catches it rather than letting it kill the turn, and passes the message to the model with an instruction to report it rather than work around it.

What to check

  1. Read the text after the colon. It is the thrown error's own message and usually names the resource.
  2. Note which tool it was. A built-in tool crashing is a defect worth reporting; a plugin tool crashing is the plugin's business, and the plugin's own log scope is where its detail lands.
  3. Check the arguments. A malformed argument object is reported separately as a bad-arguments error rather than as a crash, so a crash is not a model formatting mistake.
  4. Check for a filesystem race. A file removed between a directory listing and a read is the ordinary way a filesystem tool throws.

When it is not in this list

Tool problems are best reported from the tool call record, because that record is the only place the arguments, the status and the result are all present at once. Include:

  • The tool name and its arguments, exactly as the model sent them.
  • The tool status — ok, error, denied or running — and the full result text rather than the preview.
  • The run's workspace path, which is the root every refusal is compared against.
  • The employee and their grant list, for anything denied.
  • Whether the tool in question is built-in or plugin-provided, because the confinement rules differ and so does the responsible log scope.

Send it through contact. For shell questions specifically, include whether the approval was declined or timed out, because those two look identical in the tool result and completely different in the approvals log.

Linked from

Did this page answer your question?