Open-source project
FSMargoo/dsh-at-file avatar
FSMargoo/dsh-at-file

The install command for dsh-at-file fetches a tarball from a different organisation, and eleven of its twelve peer dependencies are wildcards

Codex-style @file mentions for DeepSeek Harness: search workspace files in the composer and attach their path to prompts.

517 stars30 forksJavaScriptMIT

At a glance

What is it?
A plugin that adds Codex-style file references to the DeepSeek Harness web composer: type an at-sign, search the workspace, and a workspace-relative path marker goes into the prompt instead of the file contents. The design is careful, including symlink loop handling and path-traversal refusal. It has also been superseded by the host, and it fetches itself from somewhere other than where you are reading it.
Who is it for?
Read this if you already have it installed and want to understand what it sends to the model, since the path-only design since version 0.3.0 is a genuine improvement over inlining file contents. Do not install it new, because the host now ships the feature and the project says so itself.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 35 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The install command fetches from a different organisation

Read the install line carefully, because the URL is not the repository you are reading:

sh
dsh plugin --profile web add https://github.com/omdsh-dev/dsh-at-file/archive/refs/tags/v0.7.0.tar.gz

The repository described in this README belongs to one account. The tarball the documented install downloads belongs to a different one.

Two things follow. First, provenance: the code a user installs is not the code in the repository the documentation describes, and nothing visible explains the relationship between the two, whether one is a fork, a mirror, a distribution point or a former home.

Second, mechanism: the install is a versioned tarball fetched over HTTPS by the plugin manager, pinned to a tag. Pinning a tag is good practice and rules out silent drift. But it is a tarball rather than a registry dependency, so it arrives with no lockfile, no integrity hash the user can check, and no transitive dependency resolution. Whatever is inside that archive is what runs.

The same command is also the update command, so the same provenance applies to every upgrade.

There is a mechanical reason the tarball can work at all, and it is worth noting because it explains the repository layout. The install downloads a git archive of a tag, and that archive only contains committed files. For a plugin whose entry point is compiled output, the output has to be in version control. That is why a built directory appears at the repository root, and it is a legitimate pattern for tarball-installable plugins even though committing build output is normally discouraged.

One more install requirement: the host process has to be restarted so both the Host and the browser client pick up the new version.

The host shipped this feature natively and the plugin says so first

The very first block of the README is an important-notice callout, and its content is that this project should not be installed new.

It states that the latest official DeepSeek Harness release now includes built-in reference features for files and sessions, that new installations should prefer the official implementation, that this plugin remains available for existing setups, and that it will be maintained occasionally on a best-effort basis.

That is a supersession notice, written by the plugin's author, at the top of the page. It is the correct disclosure and it is the first thing a reader should act on.

It also reframes everything below it. The path picker with ranked matching, per-workspace regex filters, a column-free but carefully specified result presentation, a documented icon taxonomy, and a three-layer index limit are not the features of a project waiting for adoption. They are the accumulated surface of a project whose upstream equivalent now exists, which is a different thing to evaluate.

There is no release history to check against, since the repository has no published releases, so the only version marker is the one in the manifest and the tag pinned in the install command, both of which agree on 0.7.0.

The maintenance posture in that notice is the thing to plan around. Occasional best-effort maintenance on a plugin that patches three extension points inside a host's own user interface, against eleven wildcarded host packages, is a different risk profile from the one described in the rest of the document.

For an existing installation the question is worth asking on its own terms: what does this plugin still do that the host does not? The answer in the documentation is the pasted-text and reference-bar behaviour, plus whatever the host's built-in version does not yet replicate.

A custom ignore list replaces the built-in one entirely

This is the sharpest trap in the configuration, and it is documented.

There is a built-in list of directory names the index skips. It is keyed to specific tools: version-control directories, IDE metadata, dependency trees, caches and build output, covering named editor and IDE families, named build systems, a mobile toolchain, an engine, two managed runtimes, and two game engines, plus the common JavaScript and Python output directories. Three operating-system metadata files are excluded too.

That is a well-constructed default list, and building it against named tools rather than generic patterns is the right approach.

Now the configuration option. `ignoreDirs` replaces the built-in list. Setting it to an empty list indexes every directory. And when you provide it, you are told to include every directory name you want excluded.

So the option is a replacement, not an extension. A user who wants to exclude one extra directory has to copy the entire built-in list into their configuration and add their entry, because omitting a name from their list means it gets indexed.

The failure mode is specific and unpleasant. Indexing a version-control directory and a dependency tree in a real repository means the walk traverses tens of thousands of entries, the index cache is invalidated constantly, and the picker fills with dependency filenames. The user then raises the index limit, which makes the walk slower, and the menu still shows the same fixed number of candidates.

The documentation does say to include every name, so this is a documented behaviour rather than a bug. It is worth stating plainly because the option name suggests an extension and the semantics are a replacement.

Eleven of twelve peer dependencies are wildcards, and the host framework is a release candidate

The manifest's dependency block is the part to read before trusting any of the compatibility claims.

There are more than twelve peer dependencies. Exactly two carry a version constraint. One is the plugin framework, declared with a floor that points at a release candidate, and the other is React with a conventional caret range.

The other ten or so are every one of them a wildcard. That covers the host's agent package, its protocol package, its client runtime, its client store, its remotes module, three separate user-interface packages, its locale module, its connection module, and a type registry.

So the plugin declares compatibility with every version of every internal package it depends on, including packages it injects itself into.

That is the opposite of what a peer dependency range is for. A wildcard tells a package manager that any version will do, which means the plugin installs cleanly against a host release whose internal interfaces have moved, and the failure surfaces at runtime as a missing export or a changed signature rather than at install time as a resolution error.

The release-candidate floor is its own issue. A caret on a pre-release version does not behave the way it does on a stable one: it admits later pre-releases of the same line and above, and it does not admit the earlier stable release. So a user on the current stable framework does not satisfy this requirement, and the plugin is coupled to a pre-release of its own host framework.

Against that, the prose is more disciplined than the manifest in one place. The documentation states that this version supports two specific host client package layouts, by number. So the text makes a narrow compatibility claim that the manifest's wildcards contradict.

A pasted at-path becomes a prompt marker unless you turn a setting off

Pasted text is the one path into this plugin that the user does not go through deliberately, so it is where the interesting behaviour is.

By default, pasted text is treated as ordinary text. An at-path copied from another application does not open the picker, does not appear in the reference bar, and does not create a reference marker. There is a setting to turn that off, and turning it off restores the older behaviour where such a path is parsed.

So the default is the safe one, and the risk lives entirely in the setting.

The reason the default matters is the nature of the feature. A typed token or a picker selection is a reference the user saw and accepted. A pasted token is not: someone pasted text from a website, a chat, a bug report or a document, and the plugin decides whether a substring of it becomes a path the agent is told to read. That is a prompt-injection surface by construction, because the content comes from outside the workspace and the user did not author it as a file reference.

What makes it worse is that the setting's name reads as housekeeping. It is presented as an option about whether mentions in pasted text are ignored, sitting in a panel about file mentions, next to filters and column settings. Nothing in that framing says the consequence is that pasted text will start injecting path markers into prompts.

The mitigation elsewhere in the design does hold. The Host accepts workspace-relative paths only, and absolute paths and paths that escape the workspace are ignored. So a pasted reference to something outside the workspace does not resolve. That bounds the damage considerably: the attacker gets the agent to read a path inside the workspace, not anywhere on the filesystem.

It is still the setting to leave alone unless you know you need it.

File names become XML in the prompt, and the filters accept JavaScript regex

The mechanism is worth stating in full, because it is small and it is the whole product.

You type an at-sign, the picker opens, you choose a result, and before the agent starts a step the plugin confirms the path exists inside the active workspace. It then adds a short reference message to the conversation:

xml
<workspace-reference path="docs/spec.pdf" kind="file" />

Two properties of this are worth naming.

The first is what is deliberately absent. The plugin does not open the referenced file and does not list the contents of a referenced directory. The agent inspects the path with whatever tools the session already has, if the task calls for it. The README is explicit that file format and file size do not change the behaviour, so a PDF follows exactly the same flow as any other workspace file.

That is the design change at version 0.3.0. Earlier releases read file content during submission and enforced a file-size limit. So the project moved from inlining content into the prompt to passing a pointer, which is a meaningful improvement in both token cost and data exposure, and it disclosed the boundary.

The second is that a file name becomes markup in a prompt. The token grammar stated in the documentation forbids whitespace and forbids a second at-sign character. Nothing says whether angle brackets, quotes or ampersands are filtered or escaped before the path is placed inside an XML attribute.

The filters raise the same question from the other direction. A workspace rule can be a regular expression, run by the Host against the complete base name. That is a user-supplied JavaScript regular expression executed against every file name during the index walk. The documentation says an invalid expression is reported before saving and is also rejected by the Host, which is good. It says nothing about a valid but pathological expression, and the walk is on the request path for every search.

Configuration lives in three places, and the built build ships in the tarball

The plugin's settings are split across a web panel and a patch file, and the split follows no obvious principle.

The file-name filters live in a settings panel in the web interface. They have a global scope shared by every workspace and a workspace scope for additions, and the panel shows which global rules a workspace inherits. Each rule has its own matching mode and its own case-sensitivity toggle, with case-sensitivity off by default. There are two matching modes: an exact mode that matches one complete base name and refuses path separators, and a regular-expression mode that runs against the complete base name and is explicitly given neither the parent directory nor the workspace path. Restoring defaults resets the global list; clearing workspace rules removes only that workspace's additions.

The index options live in a different file entirely, in the profile's patch configuration under the usual profile path. Setting an index limit and replacing the directory ignore list are both patch-file options, edited by hand.

The pasted-text behaviour is a third place, a toggle in the same panel as the filters.

So there are three mechanisms: a Host-connection write from the web panel, a hand-edited YAML file, and a checkbox. The documentation explains each but not why they are separated, and a user configuring this has to know which surface owns which setting.

Two smaller details round out the packaging. The manifest ships six things: the built output directory, the patch file, a plugin descriptor, both readmes and the licence. The entry point is that built output, and the exports map publishes three subpaths plus the package manifest, one of which is an invariants module that the README never explains and that exists only because the host exposes one.

The build and test wiring, by contrast, is in good shape. There is a single check script that runs a type check, the test suite and the build in order, which is a real gate rather than three separate commands a contributor has to remember.

The index has three limits and a typed path bypasses it entirely

Three separate limits govern the picker and the documentation does not explain how they relate.

The index holds at most a configurable number of workspace entries. The example configuration sets it to ten thousand. The scrollable menu shows at most fifty candidates. And the index is cached per session for thirty seconds.

So on a repository large enough to hit the ceiling, raising the entry limit makes the walk slower, the cache expires on a fixed short cycle, and the menu still offers fifty results. Nothing in the documentation says the fifty is fixed rather than configurable, or what happens at the boundary.

One design decision in there is worth crediting. Filters are combined during the Host's index walk, before entries count toward the limit or reach the browser. That means the entry budget is spent on files the user actually wants rather than on files a filter is about to discard, which is the expensive mistake to get wrong and the one this avoids.

The other decision is that the index is a convenience rather than a gate. A manually typed path can still be referenced when it exists inside the workspace, whether or not it appears in the index. So an incomplete or stale index degrades the search experience without breaking the feature. The reference dock follows the same principle: it renders only tokens that exist in the current session's settled index, and an unknown at-token stays ordinary text rather than becoming a broken reference.

Symlink handling is also specified rather than incidental. The picker indexes regular files, directories and symbolic links. A directory link is traversed through its workspace-relative alias, while a link pointing back at an ancestor stays visible without being re-entered, which is what stops a symlink cycle from becoming an infinite walk.

One gap in the token grammar follows from the other rules. A token cannot contain whitespace or a second at-sign. Since the picker will happily show and let you select a file whose name contains a space, and the documentation says format and size are irrelevant, what a space-containing selection becomes is not described.

Editorial conclusion

Read this if you already have it installed and want to understand what it sends to the model, since the path-only design since version 0.3.0 is a genuine improvement over inlining file contents. Do not install it new, because the host now ships the feature and the project says so itself. Verify first where the install command actually fetches from, since it is not this repository, and check whether the file-name filter regexes on your workspace could slow the index walk.

Frequently asked questions

Should I install dsh-at-file on a new DeepSeek Harness setup?

No. The README's first block states that the latest official DeepSeek Harness release includes built-in file and session reference features, recommends the official implementation for new installations, and describes this plugin as available for existing setups and maintained occasionally on a best-effort basis.

Does dsh-at-file send file contents to the model?

Not since version 0.3.0. The plugin writes a workspace-relative path and its kind into the conversation as a short reference marker, and explicitly does not open the referenced file or list a referenced directory. The agent inspects the path with the tools already available in the session. Earlier releases read file content during submission and enforced a size limit.

What does DSH mean in this project's documentation?

The README never expands the acronym. It uses the abbreviation for the Harness itself and for its moving parts: restarting the web command so the Host and browser client load a new version, writing settings into the web profile, and the profile directory path under the user's home directory.

Why is dsh-at-file installed from another GitHub organisation?

The documented install command points at a tarball under a different account than the repository hosting this README, pinned to a release tag. The same command is used for updates. Nothing visible explains the relationship between the two locations, so the code you install and the code you are reading are not provably the same code.

What are the file filters in dsh-at-file and where are they configured?

File-name filters live in a settings panel in the web interface, in a global scope shared by all workspaces plus per-workspace additions. Each rule is either an exact base-name match, which refuses path separators, or a JavaScript regular expression run against the complete base name with no access to the parent directory, and each has its own case-sensitivity toggle, off by default. The index options, by contrast, live in the profile's hand-edited patch file.

Official sources

  1. FSMargoo/dsh-at-file on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fsmargoo-dsh-at-file.svg)](https://hysenlabs.com/projects/fsmargoo-dsh-at-file)