Weaknesses, ranked
Warts, ordered by how much they should affect a decision to adopt or trust. Two recurring themes organize most of the individual findings; they come first.
Theme A: one security boundary carries the whole load
The single most important thing to understand before trusting dsh as a sandbox: there is exactly one real enforcement boundary — the kernel file/process sandbox — and several capabilities live outside it. Consolidated from capabilities-and-security.html:
- No command-level gate on the shell. The model runs any command with no
prompt under the default mode, as long as file writes stay in the workspace;
the sandbox does not cover the network at all
(
packages/shell/tool-bash/src/index.ts:6TODO;packages/sandbox/sandbox/src/index.ts:26-27). run_coderuns outside the file sandbox — a worker thread with the full Node API reachable (packages/code-runtime/code-runtime-worker-thread/src/index.ts:378-387).- No authentication on the web surface — loopback-only by construction is
the entire model (
packages/client/connection/src/api-request-trust.ts:12-13). - The approval channel is absent in headless, so every escalation deterministically denies there (only ACP and web mount an answerer).
- One env var opens everything —
DSH_PERMISSION_MODE=danger-full-accessremoves the sandbox and the prompts together (packages/shell/bash-sandbox/src/index.ts:91-93).
None of this is hidden — every gap is documented in a nearby code comment. But a reader who takes "sandboxed agent" at face value will substantially over-trust the system. Treat dsh's confinement as "keeps file writes in the workspace on a trusted machine", not "safe to point at hostile input".
Theme B: the gates verify freshness-of-generation, not truth-of-generator
dsh has the most elaborate doc-and-quality machinery we have seen in a production repo — and the drift we found clusters precisely inside the generated and gated artifacts, because a freshness gate proves a document matches its generator's output, never that the generator is right:
- The four capability/tool docs drift from code in eight places
(table in capabilities-and-security.html),
including two generated docs that disagree with each other about the LSP
package name — despite
docs/capability-seams.md:471claiming a "completeness guard". - Two "Generated — do not edit by hand" diagrams are actually hand-curated
and have silently drifted from the code they describe.
docs/agent-lifecycle.mdplaces prompt assembly afterstep/start(code does it before,agent.ts:230vs:279) and reversesstep/endandagent/request-error; the file's own line 1 says "Generated" while line 82 says "Maintenance mode: curated". Nothing regenerates them, so nothing catches the drift. docs/module-graph.mdhides the one real dependency cycle by drawing only peer-dependency edges (:6), omitting the value import that closes theapi-gateway → client-connection → host-apiproxy → api-remotes → api-gatewayloop.- A code comment cites a
docs/testing.mdphrase that does not exist (vitest.config.ts:269— "excessive tests are welcome"; grep finds nothing). - A phantom "golden truth" doctrine is cited inside the very skill library
that defines the dead-citation test (
.agents/skills/dsh-find-simplifications/SKILL.md:12).
The lesson is not that the machinery is bad — it is better than most. It is that automation created a false sense of coverage: the gates are load-bearing for format and freshness and silent about correctness, and the team's trust in green gates let generator bugs and hand-edits of "generated" files through. Their own postmortem 0002 states the same principle from the test side: "A snapshot refresh is fixture production, not correctness review."
Individual findings
1. Two god files
packages/host/apiproxy/src/api-proxy.ts is 3,744 lines holding the entire
host API surface — larger than the three core loop packages combined.
packages/core/tools/src/index.ts is 1,946 lines holding registry, scoping,
presentation, schema validation, code-mode transport, approval, and the whole
execution pipeline in one class with five parallel WeakMaps. Both are the
kind of file that becomes a merge-conflict magnet and a comprehension tax at
this velocity.
2. CI has live holes, acknowledged in TODOs
No hosted post-merge validation right now (serial-linux/serial-macos are
if: false), zero hosted macOS signal anywhere, and the blocking Windows
check runs under Wine on Ubuntu while the real-Windows job is non-blocking — so
a real-Windows-only regression can merge (ci.yml:338-342, :620-622). For a
product shipping a Windows CLI with ACL sandboxing, this is a genuine gap.
3. The 100% coverage gate is hollowed out for the client half
51 of 69 coverage exclusions are packages/client/*, plus extensions,
self-modification, and the Typert generator, with four TODO(gui) debt blocks
and one negated-glob exclusion (vitest.config.ts:169-268). The headline
"100% or it doesn't merge" is true for host/core and materially untrue for the
GUI surface — which is also the lead product surface.
4. Advertised surfaces that do not ship
--profile tui appears in --help and the README but has no bundle; it is a
third-party GitHub repo (turtle-ui). --host/--trusted-host are dead flags
from the CLI (the schema rejects the only value that would activate them), yet
carry two whole test files and elaborate authority-canonicalization code. Real
engineering pointed at capability that cannot be reached through a supported
invocation.
5. The same framework bug worked around five times
Vendored Cordis emit starves later listeners on a sync throw
(vendor/cordis/src/events.ts:194-196); harness code hand-rolls the same
containment block in five places rather than fixing the framework it owns. The
duplication detector misses it because identifiers differ.
6. Unbounded loops with no core ceiling
The step retry loop is while (true) with bounding delegated entirely to
listeners (agent.ts:339); a misbehaving agent/request-error listener spins
forever. The driver then swallows every failure at its boundary with no logger
call (agent.ts:212-215), so a failed turn can produce zero operator-visible
output.
7. .agents/ is 22% of repo bytes that never ship and go unexplained
~714k English words plus Chinese twins, paid by every clone, unmentioned in the README, with a 428-file frozen-archive link graveyard (36% dead links by policy) and internal telemetry/registry hostnames and Figma URLs annotated as internal. No credentials leak, but the reconnaissance surface is real.
8. Developer-preview instability with no compatibility floor
AGENTS.md:7 and SESSION_FORMAT_VERSION = 0 state that nothing is
contractual and backends reject old on-disk formats. Eight releases in one day,
a mid-flight version reset, and an access-visibility flip between rc's. Stored
sessions have no migration path. This is honest and appropriate for a preview —
but it means anything built on today's dsh will break.
9. Fragile patterns worth watching
JSON.stringify structural equality in the load-bearing log invariant
(key-order-sensitive false failures); a non-null assertion guarding what gets
written to the durable log (agent.ts:318-319); invalid tool-call JSON
degraded silently to a raw string (tool-calls.ts:104-110); parameter schemas
open by default, forcing every tool to re-defend by hand; and scope bookkeeping
in process-global WeakMaps that split silently if the package double-loads.
10. Documentation edges the machine does not reach
BENCHMARK.md is 3 lines with no data and is governed by no gate; a
release-blocking FIXME ships in reference docs
(docs/subsystems/llm-streaming.md:597); two TODOs ride inside the generated
config catalog; three subsystem pages are missing from the hand-maintained
website manifest; and the postmortem corpus — the best self-knowledge in the
repo — is not published on their own site. Only four postmortems exist for
12,294 commits, which reads as under-recording rather than few incidents.