Fix DSH read_image "cannot get property fs without inject"
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):
- Environment: dsh
0.1.0-rc.6, Windows, web profile, reproduced on thefreedompreset — every preset that mounts@deepseek-ai/dsh-tool-fsis theoretically affected (the defect lives in the package itself); read/write/editwork normally, onlyread_imagefails — the same with any image file;- 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;
- 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):
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:
- #1782: the plugin mounts (
fiberPhase: "active", constructor without errors), but its commands and tools are completely invisible to the agent; - #2917:
dsh plugin addexits 0 and--dump-configlooks fine, then the harness does not start at all — because--dump-configcomposes 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:
- Check the error shape: calling
read_imageyieldscannot get property "fs" without inject, and in the same sessionread/write/editall work; - Rule out plugins: this is a defect inside
@deepseek-ai/dsh-tool-fs— installing any plugin does not change it, so do not blame the plugin you just installed; - Wait for the upstream fix: the community pinned the call site (
src/index.tspassesimageCtxintoapplyReadImageTool, whose executor accessesctx.tools/ctx.fs); after upgrading to a dsh release that contains the fix, verifyread_imagereturns the image directly to the model; - 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:
- Pass the outer
ctx: it declares "execution depends onfs/tools;attachmentsis just the registration gate" — matching the function's documented contract and the executor's existing runtimectx.get("attachments")/ctx.get("llm")lookups:
ctx.inject(["attachments"], () => {
applyReadImageTool(ctx);
});
-
Widen the inner inject to
["attachments", "fs", "tools"]: it works, but the meaning is wrong —fsdoes not need to wait forattachmentsto be ready; if someone later adjusts attachments' availability, this wiring dragsfsresolution timing along with it. -
Regression test requirement: the existing lifecycle test proves only that
read_imageappears/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 composedToolFsplugin and require a successful image result, preserving three paired assertions: attachment mount → tool visible and executable; attachment disposal → onlyread_imagewithdraws; remount → visible and executable again.
Notes
- Do not blame the plugin you just installed — this is a defect in DSH's own
dsh-tool-fs, unrelated to plugins. - 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. - When debugging image issues, #4615 (
dsh-llm-pi-aisending 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. - 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
@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).
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.
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.
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.
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
- deepseek-harness Discussion #4612: native read_image tool fails with "cannot get property 'fs' without inject" (all presets)· deepseek-ai (GitHub Discussions)
- deepseek-harness Discussion #1782: plugin mounts but its commands and tools are invisible to the agent· deepseek-ai (GitHub Discussions)
- deepseek-harness Discussion #2917: dsh plugin add exits 0 and --dump-config looks fine but the harness won't start· deepseek-ai (GitHub Discussions)