Samuka007
dsh-nix
Nix-native packaging for DeepSeek Harness (dsh)
- Stars
- 2
- Language
- Nix
- Created
- Aug 13, 2026
- Updated
- Aug 15, 2026
Introduction
dsh-nix
Nix-native packaging for DeepSeek Harness (dsh):
pkgs.dsh— the CLI as a reproducible Nix package (pnpm monorepo build, upstream pinned at47f9438).- Profile packager — compose DSH profiles from three kinds of plugins in
one ordered list, with build-time resolution instead of
dsh plugin's activation-time pnpm reconcile. programs.dshHome Manager module — declarative plugin management in the Thunderbird style: the module owns the composition under~/.dsh, the app owns its data.
Plugin model
A profile's plugins list accepts three kinds, mixed in any order:
| Kind | Example | Resolution |
|---|---|---|
| in-box bundle | "@deepseek-ai/dsh-base" | name only; resolved from the dsh installation |
| pnpm spec | "github:someone/plugin" | fixed-output derivation runs pnpm add at build time; pin with specsHash |
| Nix package/path | pkgs.fetchFromGitHub { ... } or ./my-plugin | symlinked into the profile; carries its own dependency closure |
Layer registration (which plugins join dsh.profile.bundles) is a pure
build-time reconcile that reads each resolved package's dsh.bundle.patch
declaration — dsh plugin's install/resolve/reconcile is replaced wholesale,
so removal is declarative and resolution failures surface at nix build.
In-box profiles get a build-time boot check: checks.profile-boot-web
and checks.profile-boot-headless boot the composed profile with dsh's own
boot() (which runs assertEntriesActivated) inside the Nix sandbox and
dispose immediately. A profile that would fail at runtime — missing
services, failed activation — fails nix build instead. The check is the
real dsh fail-loud, not a reimplementation: scripts/check-profile.mjs
loads the profile exactly as dsh --profile would and lets dsh's own
audit decide. checks.profile-boot-web-nobase proves the check catches a
web-app-without-base composition (8 pending entries).
Usage
Overlay
# nixos configuration:
imports = [ inputs.dsh-nix.nixosModules.default ]; # or:
nixpkgs.overlays = [ inputs.dsh-nix.overlays.default ];
# now pkgs.dsh is available everywhere:
environment.systemPackages = [ pkgs.dsh ];
The Home Manager module's package option defaults to pkgs.dsh when the
overlay is applied, falling back to a self-contained callPackage build.
Home Manager
inputs.dsh-nix.url = "github:Samuka007/dsh-nix";
inputs.dsh-nix.inputs.nixpkgs.follows = "nixpkgs";
# in your home-manager config:
imports = [ inputs.dsh-nix.homeManagerModules.dsh ];
programs.dsh = {
enable = true;
profiles.headless = {
plugins = [ "@deepseek-ai/dsh-base" "@deepseek-ai/dsh-headless" ];
};
profiles.web = {
plugins = [ "@deepseek-ai/dsh-base" "@deepseek-ai/dsh-web-app" ];
};
profiles.custom = {
plugins = [
"@deepseek-ai/dsh-base"
"github:someone/cool-plugin" # build-time resolve
];
specsHash = "sha256-..."; # pin the resolution
userPatchesFile = ./patches.yml; # profile-level cordis.patch.yml
};
homePatchesFile = ./home-patches.yml; # -> ~/.dsh/cordis.patch.yml
# settings = { ... }; # seeds ~/.dsh/settings.yaml once
};
Activation materialises each immutable profile into
~/.dsh/profiles/<name> (stamp-based idempotent refresh). The module owns
only the composition layers; sessions, settings, and credentials under
~/.dsh remain the app's.
dsh --profile headless "task"
dsh --profile web # http://127.0.0.1:3080
Standalone
nix build .#packages.x86_64-linux.dsh # the CLI
nix build .#packages.x86_64-linux.tui-spec # example: spec-resolved profile
nix eval .#profiles.tui-spec --json # the declaration
The package also ships dsh-acp-demo, the ACP automation server app
(JSON-RPC over stdio; supply a leaf cordis.yml via --config, e.g. the
upstream examples/acp-agent/cordis.yml).
Verification
nix flake check # artifact shape + module assertions
./scripts/profile-smoke.sh # boot tui profile with packaged dsh,
# assert activate/dispose lifecycle
./scripts/hm-e2e.sh # module eval -> activation -> boot
Real agent runs need credentials (DEEPSEEK_API_KEY, or configure the model
in the web UI so ~/.dsh/.credentials.yaml is populated).
Notes
- The Home Manager module's default
packagebuildspkgs/dsh.nixagainst your nixpkgs, which must providefetchPnpmDepsandpnpmConfigHook. - Spec-string plugins change their resolved content only when their
specsHashis updated — deliberate lockfile-style ceremony. - Upstream
dshships no TUI surface (onlywebandheadlessprofiles); a third-party TUI bundle would slot in as one plugin entry.
License
MIT