dsh-direnv
Manifest validLoad each workspace's direnv environment into every DeepSeek Harness command. Make each workspace's subprocesses automatically inherit that workspace's direnv environment.
dsh-direnv
dsh plugin add dsh-direnv
Every command the harness runs then inherits the direnv
environment of its own working directory. Two minutes to install; needs
direnv on PATH.
~/.dsh $ dsh web # no .envrc here
└── session in ~/projects/api
└── bash: build → has `cargo`, `DATABASE_URL`, …
Install
- Run
direnv version. If it fails, install direnv first — this plugin never runsdirenv allowfor you. - Add the plugin:
# once it is on npm
dsh plugin add dsh-direnv
# from a checkout instead: `npm install` runs the build that produces lib/
npm install && dsh plugin add "file:$PWD"
- Confirm the patch landed. Expect
disabled: true:
dsh --profile <name> --dump-config | grep -A3 'id: subprocess'
Check it works
- Open a session in a workspace with an approved
.envrc. - Ask the model to run
bash -c 'echo $SOME_VAR_YOUR_ENVRC_SETS'. - Read the direnv tab in the right sidebar: state, variable names, and the
PATHdiff in priority order.
An empty line from step 2 means the environment is not loading. Go to Troubleshooting.
What it does
The plugin takes over the harness's subprocess seam and evaluates
direnv export json per working directory. Three flows share that one fact
source and never block each other:
| Flow | Behaviour |
|---|---|
| Injection | spawn looks the directory up in a cache and merges the diff into the child's environment. The lookup is synchronous and never waits: an unknown directory is forwarded untouched while its evaluation runs in the background, and spawnTerminal (a PTY you opened) awaits the evaluation first. |
| Gate | While the session's environment is unknown, tool calls are held — the model may talk and may ask for tools, but those calls wait as though the tool itself were slow. The gate arms once per session, waits at most loadTimeoutMs, and is a delay rather than a filter: a determinate result, the timeout, and a cancellation all release it. A released call does not cancel the evaluation; that keeps running under its own budget (evaluateTimeoutMs, 0 = never kill). |
| Surfaces | One sourced notice tells the model what was loaded. The right-sidebar tab tells you the variable names, the PATH diff (added, removed, unchanged) and the credential list, drawn entirely from DSH's own UI components so it follows your theme. |
Direnv does the walking: the anchor is each spawn's own resolved cwd, so a
command that cds into a subdirectory gets that subdirectory's environment, and
a change to any .envrc takes effect on the next tool call.
Configure
- id: direnv-subprocess
name: 'dsh-direnv'
config:
enabled: true
direnvPath: '' # empty = resolve `direnv` from PATH
loadTimeoutMs: 300000 # tool calls wait at most this long, then proceed
evaluateTimeoutMs: 0 # kill the direnv child after this long (0 = never kill)
gateTools: all # all | spawning | none ('spawning' currently behaves like 'all')
injectSensitive: all # all | filter
notifyModel: true # one sourced message per workspace state change
sidebar: true # right-sidebar tab and composer placeholder
disabledDirs: [] # absolute paths to leave alone
injectSensitive: filterwithholds variables whose names look like credentials (/KEY|PASSWORD|SECRET|TOKEN/i). Cautious setups only; it breaks dev shells that need those values.loadTimeoutMsis the gate's budget: how long a tool call may be held before it runs with whatever is known.evaluateTimeoutMsbounds thedirenvchild.0never kills it, because a colduse flakehas no predictable duration. The price: an.envrcthat never finishes keeps onedirenvprocess per directory alive for the session.
Troubleshooting
Almost every failure here is quiet. Read startup output before assuming it works. These five cover nearly all of it:
| What you see | What it means | What to do |
|---|---|---|
dsh: skipping profile bundle "dsh-direnv" | Peer range mismatch, so the whole bundle was skipped. Its patch never ran and --dump-config looks identical to a clean install. | Install a DSH the range accepts, or accept the risk with dsh plugin allow-version. |
dsh: disabling profile plugin row "direnv-subprocess" | The row-level preflight disabled it. --dump-config does not show this. | Same as above. |
dsh: warning: N entries did not activate + … pending (waiting for service: subprocess) | The patch applied but the plugin never came up, so commands have no bash tool at all. | Look for direnv-subprocess (…): failed to import; check the package is installed and its exports resolve. |
Error: service "subprocess" has been registered at <LocalSubprocessRuntime> | Both patch rows were not applied together, or the patch missed its target. | Keep both rows, keep the bundle after @deepseek-ai/dsh-base. |
| The web app exits non-zero without printing a URL, right after a checkout install | exports point at lib/, which only exists after a build. | Run npm install (its prepare builds) or npm run build. |
Two more
| What you see | What it means | What to do |
|---|---|---|
patch: entry "subprocess" not found (only in --dump-config stderr) | The bundle was composed before the row it patches. | Move it after @deepseek-ai/dsh-base in dsh.profile.bundles. |
| The right-sidebar tab never appears | The client half was dropped silently. | dsh.client.platform must be exactly "web", and every dsh.client.inject entry must be an exact package name — a <pkg>/client suffix is not normalized and matches nothing. |
--dump-config does not predict everything: it composes patches without the
row-level preflight. Patch warnings appear only there; compatibility refusals
only at startup. Read both.
If the plugin is up but a workspace has no environment, open the sidebar tab: it
reports blocked, unreadable and absent separately, and says when a
directory is merely indistinguishable from one without an .envrc.
Security
- A
.envrcis arbitrary code. Evaluating one runsbashas you, outside any sandbox. The plugin only evaluates files direnv would already evaluate, and never approves one for you. - Values never reach the model as text. The notice carries counts, paths and warnings; the sidebar is where you inspect names and reveal values on demand.
- Credential-shaped names reach commands by default. The overlay sits on top of the harness's scrubbed environment. That is the point of the plugin — and why
injectSensitiveexists. - The evaluator strips every
DIRENV_*from the environment it hands todirenv, so a harness started from a direnv-loaded shell cannot resurrect variables the harness scrubbed. - The status route is fenced. Requests go through the harness's own connection check, unlike the unfenced
/plugins/*carrier.
Limits
- MCP servers and host-side helpers bypass the seam. Anything spawning through the MCP SDK or the host process does not see the workspace environment. Covered: the harness's
shell,subprocess, search, git and PTY consumers. - Another subprocess provider would be shadowed. Only one provider holds the seat; the plugin reports loudly when it loses it.
- Some directories look empty. An unreadable
.envrc(chmod 000) or adirenv denyed directory produces the same empty output as "no.envrchere". - A failing
.envrccan half-succeed.direnvexits0forsourcemisses and syntax errors with earlier variables already applied. Both the sidebar and the notice report these as degraded. - The harness's name filter wins twice. Ambient names that merely contain
KEY,PASSWORD,SECRETorTOKEN(includingTURKEY_MODE) are scrubbed before an overlay applies. Only values a.envrcsets explicitly survive.
Three more
- Evaluation is not free.
direnvkeeps no cross-process cache, so ause nixworkspace costs roughly 0.2 s of re-evaluation per tool call (seconds when cold). Cheap directories are validated by re-fingerprinting instead: content hashes for.envrc,.env,direnv.toml,lib/*.shand the.direnv/files nix-direnv rewrites;mtime+sizefor ordinarywatch_filetargets. - A hanging
.envrcis not killed by default. Set a finiteevaluateTimeoutMsif you would rather bound it. - A timeout or abort is "stop waiting", not "no side effects". The whole process group is SIGKILLed —
direnv, the bash that sourced the.envrc, its descendants — but the.envrcmay already have written files or started a service. Nothing here prevents an.envrcfrom running; that is what the allow library andenabled: falseare for.
Development
npm run typecheck # three projects: host, browser half, tests and harness
npm test # unit tests: evaluator (real direnv) + host wiring
npm run test:client # client half: boot row, byte-identical module, and the fenced status route
test/harness/run.sh --all # end-to-end cases, no API key needed
Strict TypeScript, no build on your side for the published package: src/**/*.ts
compiles per file to lib/ (host half), and client/**/*.ts is bundled into the
single file lib/client.js the browser is served. npm install builds through
prepare; lib/ is not committed. DESIGN.md records the architecture and the
measured evidence behind every decision.
npm test uses an explicit test/*.test.ts glob on purpose: bare node --test
would also execute the scripts under test/harness/. Invoke the harness through
run.sh, not node test/harness/run.ts — the wrapper scrubs
DSH_HOME/DSH_PROFILE_DIR, and the harness refuses to run against a live DSH
home by design. The harness drives a stub LLM against dsh headless, so a real
turn with real tool execution runs offline.
Trying it by hand — a scratch home, no writes to ~/.dsh
# build the checkout once (`npm install` runs the build through `prepare`)
cd /path/to/dsh-direnv && npm install
# a demo workspace in a scratch home
export DSH_HOME=$(mktemp -d)/dsh-home && mkdir -p "$DSH_HOME" /tmp/demo/proj
cd /tmp/demo/proj && printf 'export HANDS_ON=yes-from-envrc\n' > .envrc && direnv allow
dsh --profile demo --from-default-profile web --dump-config # seed a profile
dsh plugin --profile demo add "file:/path/to/dsh-direnv" # appends our bundle last
dsh --profile demo --no-open --port 30100 # open the printed URL
Open that URL, start a session in /tmp/demo/proj, and look for the direnv tab.
The host half can also be checked from the outside — the cookie comes from the
printed ?token= link:
curl -s -H "Cookie: $COOKIE" \
"http://127.0.0.1:30100/plugins/dsh-direnv/status.json?dir=/tmp/demo/proj&force=1"
# {"ok":true,…,"status":{"state":"ok","envrcPath":"/tmp/demo/proj/.envrc","variables":[{"name":"HANDS_ON",…}]}}
A scratch home has no model provider configured, so the sidebar renders but a real conversation will not. To see the model-side behaviour, point your own configured home at the plugin for one run — an absolute-path entry mounts the host half only (it is not a package, so it has no manifest and no sidebar tab):
cat > /tmp/dsh-direnv.yml <<'EOF'
- id: subprocess
disabled: true
- insert:
- id: direnv-subprocess
name: '/path/to/dsh-direnv/lib/index.js'
EOF
dsh --profile web --patch /tmp/dsh-direnv.yml --no-open --port 30100
# then, in a workspace with an .envrc, ask the model to run:
# bash -c 'echo $YOUR_VAR'
Never run
dsh plugin addagainst a profile your Nix configuration manages: it replaces the profile's symlinks with real files. An absolute-pathinsert(above) or the Nix-side route inDESIGN.md§6.3 are the non-destructive options.
License
MIT
Comments
Loading…
Similar plugins
by TTTPOB
Native direnv environments for DSH workspace Bash and persistent terminals
★ 0
MIT
TypeScript
Sep 12, 2026
dsh plugin --profile web add dsh-workspace-envrcby tsingshitao-nuke
Right-click a folder in Windows File Explorer to open it as a DeepSeek Harness (DSH) workspace: register, launch DSH, and switch the page to it. 资源管理器右键「在此处打开 DSH 工作区」。
★ 3
MIT
JavaScript
Aug 31, 2026
dsh plugin --profile web add dsh-set-workspaceby Momojie-S
DSH plugin: per-workspace .env injection for shell subprocesses
★ 2
MIT
JavaScript
Sep 12, 2026
dsh plugin --profile web add @momojie-s/dsh-workspace-envby wjj-8283
为DeepSeek Harness增加临时工作区功能!
★ 0
MIT
JavaScript
Sep 24, 2026
dsh plugin --profile web add @wjj-8283/dsh-temp-workspaceby ivanon
按职责把工作分派给绑定了不同模型的子代理的 DeepSeek Harness 插件
★ 0
MIT
TypeScript
Aug 21, 2026
dsh plugin --profile web add dsh-dev-crewby foxi-ui
Deepseek Harness 插件-工作区快速打开编辑器vscode、idea等
★ 0
MIT
JavaScript
Aug 19, 2026
dsh plugin --profile web add @foxi-ai/dsh-command-vscode