harness-ai-desktop
Desktop client for Harness AI — an Electron app that boots the DeepSeek Harness (dsh) agent runtime in-process on loopback, with hosted sessions, remote approvals and a gated plugin market.
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 21, 2026
- Updated
- Aug 25, 2026
Introduction
English · 简体中文
How it works · Features · Security · Build · Releases · Layout · Roadmap
harness-ai-desktop is the desktop client of Harness AI: a Codex / Claude Code–style agent workbench built on DeepSeek Harness (dsh).
The agent runtime runs inside this app, on your machine — in the Electron main process itself, bound to loopback, with no inbound port. The client adds what an agent runtime does not ship: an account, hosted session history that follows you to your phone, remote approvals, and a plugin market with a supply-chain gate in front of it.
[!IMPORTANT] Developer preview. The upstream runtime is a developer preview and states that breaking changes are expected; this client tracks it at a pinned version. Installers are on the Releases page — unsigned, each carrying a SHA-256 to verify against — or build from source.
How it works
flowchart LR
subgraph machine["Your machine"]
direction TB
ui["Embedded dsh Web UI<br/>+ Harness AI plugins"]
main["Electron main process"]
dsh["dsh runtime, in-process<br/>127.0.0.1:43110"]
files["Workspace files · credentials · session log"]
main --> dsh
ui <--> dsh
dsh --> files
end
main -.->|"outbound only"| server["harness-ai-server<br/>account · hosted sessions · market catalog"]
server -.-> phone["harness-ai-app<br/>follow · answer · approve"]
dsh -.->|"API key from the local credential store"| model["Model provider"]
Three properties hold that diagram together:
- The runtime never listens outside loopback. Everything the mobile client sees is there because this client pushed it out over an outbound connection. Nothing dials in.
- The shell talks to the runtime through one narrow adapter (
HarnessAdapter), and to the hosted service through the local/apisurface — it never reaches intodshinternals, which keeps an upstream upgrade a version bump rather than a rewrite. - Our own features are plugins. Branding, the account panel, the market panel, the native directory picker and the Windows sandbox runner are Cordis plugins layered onto the upstream profile — the same extension mechanism third-party plugins use.
What it does
Runtime hosting
| In-process boot | Composes a desktop profile from the official bundle layers and boots the dsh Host inside the Electron main process — no child runtime, no second Node installation. |
| Loopback binding | Binds 127.0.0.1:43110; an occupied port moves to the next free one, up to 20 probes. |
| Version pinning | Every @deepseek-ai/* package is referenced through a pnpm catalog, so an upstream upgrade is a one-line change (pnpm dsh:version) instead of an edit across thirty dependency entries. |
| Electron host fixes | Two upstream code paths spawn Node through process.execPath, which under Electron means a second copy of the app. Both are corrected at the seam: a child_process shim for the native directory-picker worker, and a trampoline for the Windows ACL PowerShell sandbox runner. |
Account, hosted sessions and remote control
| Sign-in | Account sign-in from inside the app; each device carries its own identity and can be revoked server-side. |
| Session hosting | Local session events are mirrored to harness-ai-server, so the same conversation can be read and continued from the mobile client. |
| Client-side redaction | Credential-shaped strings are masked before upload, sessions whose working directory hits the denylist are never synced, and large blobs never ride the event channel. |
| Attachments | Images the agent produced are content-addressed (sha256:…), uploaded on a channel of their own behind the events, deduplicated per account and capped by a server-side quota. |
| Remote approvals and prompts | A tool that steps outside the workspace raises an approval instead of executing. Settle it at the desk, or from your phone. Prompts sent while the desktop is offline are queued server-side and drained on reconnect. |
Plugin market, with a supply-chain gate
The runtime's permission system governs tool calls, not the code a plugin ships. So every defence sits before installation:
- Risk flags on each catalog entry — install scripts, native build, no provenance, no license, low adoption, new package.
- A disclosure gate in front of every install, including installs handed over from the website via
harness-ai://install?listing=<id>, stating plainly that a plugin runs with the same access as the client itself. - An integrity re-check against the registry before anything is written: the integrity hash the catalog recorded must still match. This closes the hole a pinned version number leaves open — the same version can be republished with different bytes.
--ignore-scripts, written explicitly, never inherited from a config file that might drift.- Capability inspection after install, reporting what the package actually reaches for: network, file writes, native modules.
- An install journal that records the profile manifest before every change and restores it after a failure or a crash — restoring the manifest text only, never deleting
node_modules.
Reliability
Single-instance lock · crash audit of the previous run · secret-masked file logs · system tray · a recovery page (retry / open logs / quit) when boot fails · and a boot watchdog that measures progress rather than wall-clock time (20 s without progress, 180 s absolute), so a slow machine is not mistaken for a hung one.
Updates
The client checks for a new version shortly after starting and every six hours, and downloads one in the background — the tray reports it. Nothing is ever installed behind your back. When a version is downloaded, you are asked once:
| Answer | What happens |
|---|---|
| Restart and install | The app restarts into the new version now. |
| Later | The download is kept and installs the next time you quit. |
| Cancel | The download is kept, but nothing installs by itself. The tray still offers to install it whenever you want. |
Closing the dialog is the same as cancelling. A check you start yourself always answers, including "you are up to date".
Security model
| Line | Guarantee |
|---|---|
| Loopback only | The runtime binds 127.0.0.1. No inbound connection reaches your machine — not from the server, not from the phone. |
| Model keys stay local | API keys live in the dsh credential store on your machine and are never uploaded. |
| Redaction before upload | Masking and the working-directory denylist run client-side, so the hosted service never receives what was filtered out. |
| Approvals are explicit | Work outside the workspace needs a human answer, and every decision is auditable. |
| Installs are disclosed | Nothing is installed without a gate that names the risk, and the website can only hand over a catalog id — never a package name and version. |
Found a vulnerability? See SECURITY.md.
Configuring an installed client
Some settings belong to a network rather than to a person, and being wrong about
them should not require a new build. Those live in harness-ai.config.json,
next to Harness AI.exe in the installation directory. Edit it, restart the
client, and the log line config: plugin registry … states what took effect.
| Key | Default | What it is for |
|---|---|---|
pluginRegistry | https://registry.npmjs.org/ | The npm registry the plugin market resolves and downloads from. Where the public registry is slow or unreachable, point it at a mirror — https://registry.npmmirror.com/, for instance. |
Pointing at a mirror does not lower the bar the market enforces: the integrity re-check reads from the same registry the tarball will come from, so a mirror that serves different bytes than the catalog recorded is refused rather than trusted. It must be a mirror of the public registry, though — a registry that serves its own packages under the same names cannot satisfy that check.
A malformed file never stops the client: the value is dropped, the reason is
logged, and the default applies. HARNESS_PLUGIN_REGISTRY overrides the file
for a single run. Installing an update rewrites the file with the shipped
defaults, so re-apply local changes afterwards.
Building from source
Prerequisites — Node ^22.19.0 || >=24, pnpm 11, Windows x64 (the only packaged target today), and a DeepSeek API key for anything that talks to a model.
The wire contracts this client shares with the mobile client and the hosted service come from npm as @harness-ai/contracts, so a plain clone installs and builds — nothing else from the workspace is required.
pnpm install # postinstall fetches the Electron binary
pnpm typecheck
pnpm test # 146 unit tests, offline
pnpm dev # build the in-repo plugins, then run the shell
| Command | What it does |
|---|---|
pnpm dev | Generate icons → build in-repo plugins → electron-vite dev. |
pnpm build | Production build of main / preload / renderer plus the plugins. |
pnpm typecheck | tsc --noEmit; must be clean. |
pnpm test | The Vitest unit suite — offline and fast by design. |
pnpm test:e2e | The suite that needs the network or a real registry: plugin installs, hosted-attachment round trip. |
pnpm dist:win | NSIS installer into dist/, behind a third-party-notice gate and an afterPack check. |
pnpm smoke:packaged | Boot the packaged app and assert the loopback endpoint, the runtime page and the brand plugin. |
pnpm dsh:version | Move every pinned dsh package to a new upstream version in one step. |
The release checklist, including the manual passes, is in docs/acceptance.md.
Releases
Every push and pull request runs typecheck, the unit suite and a production build on Windows — the platform the client ships on.
A release is a tag. Pushing v<version> runs the same gate, packages the NSIS installer and publishes it, together with latest.yml and the blockmap the in-app updater reads:
git tag v0.1.5 && git push origin v0.1.5
The tag must match version in package.json, and CHANGELOG.md must carry a section for it — both are checked before anything is built, and that section becomes the release notes. Installers are not code-signed — SmartScreen will warn, and the release notes carry the SHA-256 to check against.
Project layout
src/main/ Electron main: boot, tray, updater, crash audit, logging, deep links
harness/ The dsh seam — adapter, boot, hosting bridge, market and install guard
account/ Account service and device identity
src/preload/ Context-isolated bridge to the renderer
src/renderer/ Shell chrome around the embedded runtime UI
src/shared/ i18n (en-US / zh-CN) and the shell API types
plugins/
brand/ Product identity inside the runtime UI (tray, theme, sidebar)
account-ui/ Sign-in and device panel
market-ui/ Plugin market panel, risk chips and the install gate
electron-directory-picker/ Native workspace picker
windows-pwsh-sandbox/ Windows ACL sandbox runner, corrected for the Electron host
scripts/ Icon generation, packaging checks, smoke and acceptance drivers
Roadmap
| ✅ Shipped | In-process runtime hosting · account and device identity · hosted session sync · attachment sync · remote approvals and queued prompts · plugin market with the install gate · Windows packaging |
| 🚧 In progress | A stable update feed — releases ship latest.yml, but the feed host stays a sentinel until the distribution location is settled |
| 📋 Planned | macOS packaging and code signing · a second harness behind the same adapter, if and when one earns its place |
Contributing
Issues and pull requests are welcome — start with CONTRIBUTING.md. It covers the commit conventions, the English-only source rule, and how a change to upstream behaviour is expected to be staged: plugin first, upstream patch last.
Acknowledgements
Built on DeepSeek Harness (MIT) and Cordis. Third-party notices for a packaged build are generated at release time into THIRD_PARTY_NOTICES.md.
License
MIT © harness-home