Give agents access to files on the machine where the browser runs: user authorizes a local directory via File System Access, tool calls relay over the plugin's own WebSocket; read-only compat mode for mobile/insecure contexts.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-browser-fs
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:whitefirer/dsh-browser-fs
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
中文 | English
Lets dsh's agent read and write local files on the machine where the browser runs. dsh's built-in fs tools can only reach the host machine; when dsh is deployed remotely, the browser is on another machine and the agent cannot touch your local files. This plugin fills the gap:
The user authorizes a local directory in the dsh web page via the File System Access API
(showDirectoryPicker), and the handle is stored in IndexedDB; the agent works with three
model tools — list/read/write — whose calls are relayed to the browser over the plugin's own
WebSocket channel.
How it works
A two-sided plugin (cordis plugin system):
- Host half (
src/index.ts, runs in dsh's host Node process)ctx.webServer.registerUpgraderegisters the WS channel at the exact path/browser-fs/ws;ctx.tools.registerregistersbrowser_fs_list/browser_fs_read/browser_fs_write;- execute sends a
{type:'call', rpcId, op, args}frame to the tab "holding the authorized handle" and pairs result frames by rpcId;exec.signalis wired to pending-call abort (a cancel frame is also sent to the browser).
- Client half (
src/client/, runs in the browser)- On startup, reads the handle back from IndexedDB and runs
queryPermission; connects back to the host's WS (exponential-backoff reconnect); - Executes File System Access operations on the authorized directory when call frames arrive, and replies with result frames;
- Registers a floating card in the
shell.overlaylayer: shows connection/authorization status and provides 授权目录 (authorize) / re-authorize / switch / revoke buttons. - Broadcasts
{type:'state', hasHandle, dirName}on authorization changes; the host only dispatches calls to tabs withhasHandle=true(executor selection when multiple tabs are online).
- On startup, reads the handle back from IndexedDB and runs
Installation
# Install from npm (recommended; no install scripts, no build authorization needed)
dsh plugin --profile web add dsh-browser-fs
# Or install from GitHub (build artifacts are committed, zero install scripts)
dsh plugin --profile web add github:whitefirer/dsh-browser-fs
# Local development: reinstall after changes (run npm run build first; lib/ is committed)
npm install
npm run build
dsh plugin --profile web add file:/abs/path/to/dsh-browser-fs
# Restart dsh to take effect
dsh plugin add installs the package into the profile's dependencies and, thanks to the
dsh.bundle.patch manifest declaration, automatically appends dsh-browser-fs to the
dsh.profile.bundles layer stack (the patch is this repo's cordis.patch.yml: one insert
line mounts the host half; its config carries wsPath and requestTimeoutMs).
Usage
- Open the dsh web page; the "browser-fs 浏览器文件" card appears at the bottom-right (expanded by default when unauthorized; collapses to a 📁 ball after authorization — click to expand, press-and-hold to drag, "—" to collapse; collapse state persists in localStorage across reloads, and the ball's status dot matches the card's colors);
- Click 授权目录 (authorize) and pick a local directory in the system picker (readwrite permission required);
- The card's 目录内容 (contents) section browses the authorized directory directly: a lazy-loaded tree (click a directory row to expand/collapse, 200 entries per level with a "…N more" overflow line), plus a search box that recursively matches file/directory paths; file rows show sizes and a 复制路径 (copy path) button that copies the relative path — handy for pasting to the AI;
- The agent can then use the three tools:
browser_fs_list { path?, recursive? }— list a directory (relative path/kind/size, optional recursion)browser_fs_read { path, maxBytes? }— read a text file (256 KiB cap by default, truncation marked)browser_fs_write { path, content }— write a text file (parent directories auto-created, returns byte count)
The tool descriptions tell the model explicitly: these operate on the browser machine's local disk, not the host's filesystem.
The card UI language follows dsh's own Settings → General → Language switch (the plugin
subscribes to the dsh client's locale service, live; compositions without that service
fall back to <html lang>/browser language).
Preview & refresh
Click a file name in the contents tree to pop the preview window (mask + default-size window, min(720px,92vw) × min(70vh,560px); drag the title bar to move it, drag the bottom-right corner to resize; pinned title bar — file name + size/truncation note + ✕, with the relative path below; content area scrolls independently; ✕ / mask click / ESC to close):
- Images (png/jpg/jpeg/gif/webp/svg/ico/bmp): read as arrayBuffer into a blob URL shown
with
<img>(revokeObjectURL on close); images over 8MB are not fetched — a too-big notice is shown instead; - Everything else as text: only the first 64KB, UTF-8 decoded, monospace
<pre>; truncation is marked ("仅前 64KB"); decoded text containing NUL counts as binary and shows "二进制文件不支持预览" (binary files not supported).
Preview takes the same path in both modes (the full/compat backends each implement
readBlob), so it works in read-only compat mode too.
Text/code preview supports editing (full mode only, i.e. when the backend is writable):
click Edit to load the complete file into a textarea; saving goes through the same
backend write path as the agent tool browser_fs_write (FsBackend.write), then the
preview refreshes automatically. Large files are still shown as the first 64KB only, and the
editor clearly notes that the full file has been loaded and saving overwrites the whole
file. Compat mode stays read-only and does not show the edit entry. While editing, ESC first
exits editing; a second ESC closes the window. The editor shows line numbers and uses the
same syntax highlighting described below.
Text preview comes with syntax highlighting: the extension maps to a language
(js/ts/tsx/py/go/rs/java/c/cpp/h/sh/yaml/json/toml/md/html/css/xml/sql, etc.; unmapped
extensions stay plain text), and only the truncated first 64KB is highlighted. Highlighting
uses a highlight.js language subset with the GitHub Dark theme; to keep the main bundle
small it ships as a separate chunk — the host half serves it at highlight.mjs next to
/browser-fs/ws, and it is dynamically imported the first time a preview hits a mapped
language (plain text with a "语法着色加载中…" note while loading, silent fallback to plain
text on failure).
The card is draggable: the title row is the drag handle (mouse and touch; movement
beyond 4px counts as a drag, so collapse/button clicks are never eaten); a bottom handle
lets you drag vertically to resize the card height (minimum 160px, persisted under
localStorage key dsh-browser-fs:card-height); while dragging, the panel/ball tracks the
pointer directly, with clamping applied only on release and window resize. The ball always
stays fully inside the viewport (flush to edges, no hidden margins). An expanded panel that has never been dragged derives its initial position from
the ball (flipping leftward/upward when the ball sits in the right/bottom half, then
clamping into a 10px margin with width/height capped to the viewport); once dragged it
stays where it was dropped (viewport clamp only, never flipped), and the position is
remembered across collapse/expand and page reloads (same localStorage key
dsh-browser-fs:card-pos as the ball). The collapsed 📁 ball sits at the anchor (the
card's top-left); it is draggable too — a release without movement expands the card.
Card, ball and preview window render at body level (z-100/200): above common overlays
(e.g. sidebar-plugin panels) so clicks are never stolen by other plugins, yet still below
dsh's own modals (z-1000+).
The "↻" at the end of the authorization button row refreshes the directory:
- Full mode: clears all directory-tree caches (expanded set / loaded levels) and re-fetches the root level;
- Compat mode: the cache is the selection-time File snapshot and the browser allows no silent re-read, so refreshing is meaningless — the button reopens the picker instead (same as 重新选择).
Multiple devices
Multiple devices can each keep a dsh page open; the model sees "each device authorizes its own local directory":
- Each device's browser tab derives a device label from its UA (e.g. "Windows · Chrome" /
"Android · Chrome" / "macOS · Safari"); click ✏️ on the card to set a nickname (stored in
localStorage
dsh-browser-fs:device-name; the nickname wins over UA derivation). - Every tab reports
{hasHandle, dirName, label}to the host; the host maintains an executor roster and broadcasts it to all online tabs. Devices without authorization see "当前授权在设备:某某(目录名)" on the card — no more guessing where the grant lives. - Agent tool calls route to a handle-holding device: with several holders, the first-connected one wins (deterministic). Tool results and error messages carry the executor's label (e.g. "已写入 3 字节到 b.txt(设备:X)"), so the conversation shows which device ran the call.
- Authorize a directory on this machine and it joins the executors; when a device disconnects or revokes, the roster shrinks immediately.
LAN/mobile access & secure context
The File System Access API is gated behind secure contexts: it only exists under HTTPS or
localhost. On LAN http (e.g. http://192.168.0.x:9101, common when a phone goes through a
proxy), window.showDirectoryPicker simply does not exist and cannot be polyfilled. This
plugin feature-detects with typeof window.showDirectoryPicker === 'function' (not
isSecureContext — a proxy-injected polyfill may have patched it), and falls back to
compat mode when missing. Capability differences:
| Full mode | Compat mode | |
|---|---|---|
| Trigger | Secure context (HTTPS/localhost) | Automatic fallback on insecure contexts |
| Picking | System directory picker (showDirectoryPicker) | Dual entries: input[webkitdirectory] for directories / multiple for file multi-select |
| list / read | ✓ | ✓ (in-memory File map; read slices instead of loading whole files) |
| write / mutations | ✓ | ✗ explicit "兼容模式只读…" (compat mode is read-only) error |
| Grant persistence | ✓ IndexedDB, restored on reload | ✗ nothing to persist; re-pick after reload (noted in the status line) |
| Directory-tree browsing | ✓ | ✓ same path (the backend abstraction is shared) |
Mobile behavior: the compat authorization area offers two side-by-side entries — 选择目录 (pick directory) and 选多个文件 (pick files) — instead of relying on capability probing alone; the picker input is positioned off-screen (not display:none, which mobile browsers and the WeChat WebView block from programmatic clicks). On iOS, directory picking may be a dead end (the webkitdirectory attribute exists but picking a directory returns 0 files): the card then shows a clear error ("没读到文件…请改用「选多个文件」") and the directory entry silently switches to multi-select from then on — never silent. After a successful pick, the status line shows what was selected (directory name or "N 个文件").
In compat mode the card shows a 兼容模式 (compat mode) badge with an explanation. Three ways to get full mode:
- SSH port-forward to localhost:
ssh -L 9101:127.0.0.1:9101 user@host, then visithttp://127.0.0.1:9101; - In Chrome, open
chrome://flags/#unsafely-treat-insecure-origin-as-secureand allowlist the LAN origin; - Deploy HTTPS.
Limitations
- Secure context: the File System Access API requires an HTTPS or localhost context; over plain-HTTP remote access the authorize button fails with a clear error.
- A tab must stay open: with no browser tab online, tool calls fail fast with a clear error; same when tabs are online but no directory is authorized.
- The agent tools only support UTF-8 text read/write (binary writes are out of scope; images preview inside the card only — see "Preview & refresh").
- The client half always connects to the default WS path
/browser-fs/ws(the highlight chunk path derives from the same directory:/browser-fs/highlight.mjs); if the host half'swsPathconfig is changed, the client'sDEFAULT_WS_PATH(src/wire.ts) must be updated in sync and the bundle rebuilt. - The host half peer-depends on
@deepseek-ai/dsh-tools(defineTool); at runtime it resolves through the profile's flat node_modules fallback to the same copy bundled with the dsh installation.
Development
npm run build # esbuild: host half ESM + client half CJS closure (__ModuleLoader__ wrapped) + highlight lazy chunk (lib/highlight.mjs)
npm run typecheck # tsc --noEmit
npm run smoke # link self-check: call→result round-trip / abort / disconnect / cross-origin rejection (scripts/smoke.mjs)
After changing code: re-run npm run build, then dsh plugin --profile web add file:<this dir> once more (file: dependencies are packed copies, not symlinks), and
restart dsh.
Links
More in this category
Tencent/WeKnora#dsh-weknora★ 21028
Four read-only tools over a WeKnora knowledge base: list knowledge bases, hybrid passage search, reassemble one document's chunks in order, and WeKnora's own cited RAG or ReAct-agent answer with a resumable session id.
superdesigndev/treg★ 709
Tool catalog for agents: search ~2,600 external endpoints (SEO and SERP, backlinks, social, people and company enrichment, ad libraries, scraping) by the task you want done, read each one's parameters and per-call price, then call it with the credential injected server-side. Ships the skill plus an MCP row that stays disabled until TREG_TOKEN is set.
anysearch-team/anysearch-dsh★ 377
AnySearch-powered real-time web and vertical search provider for DeepSeek Harness.
EthanYoQ/Invoice-Downloader#dsh-invoice-downloader★ 285
Local IMAP invoice download, OCR, archive, and Excel reimbursement summaries for DeepSeek Harness.
omdsh-dev/dsh-data-agent★ 183
Let the AI connect to databases and write SQL for you.
zhaoolee/notes#dsh-plugin★ 155
Export DSH conversations as Smartisan Notes-style PNGs, or create and update Markdown notes in a configured account-scoped workspace.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.