Fix DSH read_image "cannot get property fs without inject"

TroubleshootingPublished 2026-08-27Author: DSH Plugin Hub
DeepSeek HarnessDSHread_imagecannot get property fs without injectimage tool error
DSH read_image errors cannot get property "fs" without inject? dsh-tool-fs scopes it to attachments; ctx.fs trips the guard. Native bug, wait for fix.

DSH's native read_image tool failing instantly with Error: cannot get property "fs" without inject, while read / write / edit all work, is a one-line scope mistake inside DSH's own package @deepseek-ai/dsh-tool-fs: apply() registers read_image inside an attachments-gated sub-context but hands the narrowed imageCtx to the executor, whose property access on ctx.fs trips the cordis guard. Installing any plugin does not change it; the temporary workaround is an external vision CLI, and the real fix arrives with an upstream dsh upgrade.

What the read_image "cannot get property fs without inject" error looks like

The error is always Error: cannot get property "fs" without inject, it appears only when calling read_image, and the image file itself is irrelevant. A reporter reproduced it in practice (discussion thread):

  1. Environment: dsh 0.1.0-rc.6, Windows, web profile, reproduced on the freedom preset — every preset that mounts @deepseek-ai/dsh-tool-fs is theoretically affected (the defect lives in the package itself);
  2. read / write / edit work normally, only read_image fails — the same with any image file;
  3. Every step of this chain looks healthy: the plugin mounts → the tool registers successfully into the model-visible catalog → the model sees it and calls it → and only at actual execution does it throw;
  4. The cost: the model burns a whole turn on a tool it has no way of knowing is broken.

Root cause: read_image registered in a narrowed sub-context

@deepseek-ai/dsh-tool-fs/lib/index.js's apply() registers read_image inside an attachments-gated sub-context and passes that narrowed context to the executor. The shape (source):

js
const inject = ["tools", "fs", "systemPrompt"]; // plugin-level declarations
function apply(ctx, config) {
  applyReadTool(ctx);            // fine: outer ctx declares fs
  ctx.inject(["attachments"], (imageCtx) => {
    applyReadImageTool(imageCtx); // ← imageCtx only declares "attachments"
  });
  // ...
}

applyReadImageTool's executor dereferences ctx.fs.readBytes(...) by property access (and registers through ctx.tools.register(...)). Cordis's guard throws on undeclared service-property access, so execution dies with cannot get property "fs" without inject. The function's own doc comment states the intended contract — "execution uses its fs service plus the optional attachments / llm services" — the call site was supposed to pass the outer scope, but passes the narrowed accessor instead. The executor already handles the optional services correctly via runtime lookups (ctx.get("attachments"), ctx.get("llm")); only the declared-service property accesses (fs, tools) are affected.

Why it passes every mount-time check

Because the guard fires only at execution time. This bug's shape deserves its own note: the plugin mounts, the tool registers, the model sees it, the model calls it — all normal, until the moment of execution. The same family in this community:

  1. #1782: the plugin mounts (fiberPhase: "active", constructor without errors), but its commands and tools are completely invisible to the agent;
  2. #2917: dsh plugin add exits 0 and --dump-config looks fine, then the harness does not start at all — because --dump-config composes but never boots.

The common thread: the "mount/compose success" signal does not guarantee the registered thing actually works. Yours is the most benign member of the family — cordis names the missing service (cannot get property "fs" without inject), which is rare here; most siblings stay silent. It is also a concrete argument for validating scope at registration time rather than at first use.

How to verify and work around it

First confirm this is the bug: only read_image crashes, the sibling tools all work, and the image is irrelevant — that is it. Step by step:

  1. Check the error shape: calling read_image yields cannot get property "fs" without inject, and in the same session read / write / edit all work;
  2. Rule out plugins: this is a defect inside @deepseek-ai/dsh-tool-fsinstalling any plugin does not change it, so do not blame the plugin you just installed;
  3. Wait for the upstream fix: the community pinned the call site (src/index.ts passes imageCtx into applyReadImageTool, whose executor accesses ctx.tools / ctx.fs); after upgrading to a dsh release that contains the fix, verify read_image returns the image directly to the model;
  4. Temporary workaround: route image reads through an external vision CLI (such as modlens), bypassing the DSH tool pipeline — this also explains why the bug went unreported for a while: people sidestep it and never come back to report.

Fix direction: a one-line change at the call site (for maintainers)

The recommended fix passes the outer ctx into applyReadImageTool rather than widening the inner inject. Both pass the guard, but they mean different things:

  1. Pass the outer ctx: it declares "execution depends on fs / tools; attachments is just the registration gate" — matching the function's documented contract and the executor's existing runtime ctx.get("attachments") / ctx.get("llm") lookups:
js
ctx.inject(["attachments"], () => {
  applyReadImageTool(ctx);
});
  1. Widen the inner inject to ["attachments", "fs", "tools"]: it works, but the meaning is wrong — fs does not need to wait for attachments to be ready; if someone later adjusts attachments' availability, this wiring drags fs resolution timing along with it.

  2. Regression test requirement: the existing lifecycle test proves only that read_image appears/disappears as the attachment fiber mounts and disposes — schema visibility does not exercise the guarded property access. A regression should call the tool obtained from the fully composed ToolFs plugin and require a successful image result, preserving three paired assertions: attachment mount → tool visible and executable; attachment disposal → only read_image withdraws; remount → visible and executable again.

Notes

  1. Do not blame the plugin you just installed — this is a defect in DSH's own dsh-tool-fs, unrelated to plugins.
  2. The model burns a full turn on read_image; suggest it read images another way (for example asking the user to describe), or use an external vision CLI.
  3. When debugging image issues, #4615 (dsh-llm-pi-ai sending non-png/jpeg/gif images as-is to a strict OpenAI-compatible backend) lives in the same "image path" neighborhood — reading both together saves time.
  4. Similar plugin errors are collected in the DeepSeek Harness plugin error collection: DSH plugin not loading, Web UI issues, and session cache repair.

Sources: Discussion #4612, #1782, #2917

FAQ

Why does DSH's read_image throw Error: cannot get property "fs" without inject while read/write/edit work fine?

@deepseek-ai/dsh-tool-fs's apply() registers read_image inside a ctx.inject(["attachments"], ...) narrowed sub-context and passes that narrowed imageCtx to the executor; the executor accesses ctx.fs / ctx.tools by property, and the cordis guard throws at execution time (source).

Why does only read_image fail while read/write/edit work fine?

read/write/edit are registered directly with the outer ctx in apply() (which declares fs); only read_image is wrapped in the attachments-gated sub-context and receives the narrowed scope — its ctx.fs access trips the guard. The image file itself is irrelevant.

The plugin mounts and the model sees the tool, so why does it fail with cannot get property "fs" without inject only on call?

Cordis's service-property guard only fires at execution time: the plugin mounts, the tool registers into the model-visible catalog, the model sees and calls it, and only then does it throw. Mount/compose success does not guarantee the registered thing actually works — see also #1782 and #2917.

How do I fix this error? Does installing a plugin help? What is modlens?

It is a one-line scope mistake inside DSH's own package; installing any plugin does not change it. Wait for the upstream fix and upgrade dsh. The temporary workaround is to route image reads through an external vision CLI (such as modlens), bypassing the DSH tool pipeline.

Is this a plugin problem or a DSH core problem? Is there a community patch?

It is DSH core (@deepseek-ai/dsh-tool-fs lives inside DSH). The community pinned the call site and proposed a one-line fix (pass the outer ctx to applyReadImageTool); the reporter verified it end to end on Windows. It is the benign end of the family — cordis names the missing service instead of staying silent.

Sources