DeepSeek Harness host exits 0 after SIGTERM: find the cause

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessSIGTERMprocess exitobservabilityhost-exit-marker
DeepSeek Harness exits 0 after SIGTERM because profile-boot maps it to interrupt(0). Add an exit marker, your own signal handler, or an outer supervisor.

When kill -TERM makes the host exit quietly with 0, that 0 does not mean everything was fine; it is the designed result of interrupt(0). profile-boot maps SIGTERM to a deliberate cleanup and exit, SIGHUP is never registered, and warning-level errors only enter an in-memory ring. A crash, a user quit, and a signal kill therefore look identical from outside (Discussion #10022).

DeepSeek Harness signal semantics and the missing external evidence

Many people assume a listener just "notes the event"; in fact it changes how the process responds to the signal. Two Node behaviors come first:

  1. Once a signal has a listener, Node stops performing its default action. A bare process.on('SIGHUP', ...) turns termination into ignore, so the host no longer exits on hangup and appears stuck.
  2. Node's signal handles are unref'd and do not keep the event loop alive. A script that only waits for a signal exits immediately after registering.
  3. A correct recorder therefore writes its record, removes its own listener, and re-raises the same signal against itself so the process dies the original way.
  4. If re-raising fails, follow shell convention and use 129, 130, or 143 for the corresponding signal instead of faking a 0.

These rules apply to any plugin that takes over signals in Node, not just DSH. The difference is that DSH's profile-boot already wires SIGTERM to interrupt(0) at apps/cli/src/profile-boot.ts:307-308; your listener only appends and cannot change that order.

Before troubleshooting, accept one fact: a default installation leaves no persistent trace to inspect. The reporter in the thread checked each item:

  1. Crash logs: neither ~/Library/Logs/DeepSeek Harness/ nor the macOS DiagnosticReports contain a new entry. Expected: nothing at the matching time.
  2. stderr: the host does not write the exit reason to any persistent file. Expected: the redirected log contains normal output only, with no line such as exiting: SIGTERM.
  3. Exit code: interrupt(0) has a 5-second grace period (PROCESS_SHUTDOWN_TIMEOUT_MS) and forces the exit after it, still with code 0. Expected: a normal quit and a kill are indistinguishable.
  4. Desktop shell behavior: the Electron main process sends SIGTERM to the host child both when you quit the app and when IPC fails, so every exit takes this path. Expected: the presence of SIGTERM cannot tell you whether the user acted.

That is why the reporter's watchdog script vanished after a restart with no record of what killed it.

DeepSeek Harness: a one-minute normal-exit versus signal-exit test

One controlled comparison confirms "the exit code is unusable" faster than reading source. Four steps:

  1. Quit the app normally from the desktop shell, note the time, and check the host process exit code. Expected: exit 0 and no new files in the log directory.
  2. Start it again and run kill -TERM <pid>, recording the same fields. Expected: still exit 0 and still no new log.
  3. Compare timestamps, logs, and DiagnosticReports for the two runs. Expected: nothing distinguishes them.
  4. Try kill -KILL <pid> once. Expected: the shell reports 137, the only run that differs, but it says nothing about what happened before the kill.

Steps 1 and 2 producing the same result is the most direct evidence that the exit code is unusable.

DSH plugin path 1: install host-exit-marker

This is the smallest change, and it gives you a readable exit ledger immediately; check the plugin page and version notes on DSH Plugin Hub. Steps:

  1. Run dsh plugin add @argszero/dsh-host-exit-marker. Expected: the plugin installs and host-exit.log appears under ~/.dsh/logs/.
  2. Restart the host and confirm the start line was written. Expected: a line like start pid=… host=… node=… startedAt=….
  3. Run kill -TERM <pid> manually. Expected: the log appends signal signal=SIGTERM pid=… uptime=… others=…, after which the process exits along its original path.
  4. Force kill -KILL <pid>, then restart. Expected: the next start writes died-without-exit and uses lastAlive to place the death within one heartbeat interval.

It uses prependListener so its record runs before profile-boot cleanup, and it does not change the exit status, the shutdown path, or other listeners. According to the thread, version 0.1.0 is published on npm and the source repository is public.

DeepSeek Harness path 2: register your own handler and write a file

This suits people who do not want a third-party plugin or need records in their own logging system. The key is not "add a listener" but "add it and then restore the default":

  1. Register SIGTERM and SIGHUP with process.prependListener when your plugin starts. Expected: your handler sees both signals first.
  2. In the handler, synchronously append one line to ~/.dsh/logs/host-exit.log with the time, pid, signal name, and uptime. Expected: the line is on disk even if the process exits immediately.
  3. Remove your listener after writing, then call process.kill(process.pid, signal) to re-raise. Expected: the process exits by the original default action instead of being held by your listener.
  4. Test SIGHUP with a child process. Expected: the child really dies from the hangup and the log has a matching record, which proves you did not turn termination into ignore.

Do not stop at process.on: that turns SIGHUP into ignore, the host stops caring about hangup, and the problem only becomes harder to diagnose.

DeepSeek Harness path 3: have an outer supervisor record exit status

This suits environments where systemd, launchd, or a custom daemon starts the host, and it needs no DSH changes. How to do it:

  1. Run the host as a foreground child under the supervisor instead of letting the shell swallow the status. Expected: the supervisor receives the real wait status.
  2. Record the time, exit code, and signal for every exit in the supervisor. Expected: at least "exit 0" and "signal 15" can be distinguished.
  3. Restart on abnormal exit and count restarts. Expected: repeated restarts in a short window raise an alert instead of looping silently.
  4. Keep an application-side log path as well. Expected: the supervisor records how it died and the app log records what it was doing before.

The supervisor has a clear limit: against interrupt(0) it still sees exit 0, which proves the process ended but not why. It complements Path 1 and cannot replace it.

DeepSeek Harness: what this approach cannot cover

An exit record answers "did it happen", not "why". Four boundaries:

  1. Warning-level plugin errors still live only in the 64 KB memory ring, out of reach of plugins; the host must mirror warnings to a rolling file.
  2. SIGKILL, native crashes, and power loss run no handler at all; only the heartbeat record can fill them in on the next start.
  3. A recorder cannot make the exit "loud"; stderr belongs to the host.
  4. The exit code does not change because a plugin is installed, so do not expect it to separate normal exits from signal exits.

Fix the troubleshooting order: check host-exit.log for a signal line, then for died-without-exit, and only then read application logs. If all three are empty, the problem is further out.

RouteWhat it showsWhat it cannot show
host-exit-markersignal name, uptime, deaths with no handlerexit code, warning ring
your own signal handlercustom fields and destinationnative crashes, SIGKILL
outer supervisorexit code, signal, restart countinternal host cause
all three combinedcause, count, and a timelinekernel-level crash details

The three can be stacked: the plugin supplies the cause and the supervisor supplies the count, and together they turn "it vanished again" into a traceable timeline.

Source: Discussion #10022, argszero/dsh-host-exit-marker. For other startup and exit entry points, see DSH troubleshooting.

FAQ

After kill -TERM the DeepSeek Harness host exits with code 0. How do I tell a crash from a signal?

The DeepSeek Harness exit code cannot tell you. profile-boot maps SIGTERM to interrupt(0), and after the cleanup grace period it exits 0, so a normal quit and a signal termination are structurally identical. The only reliable approach is to write a persistent record before the process leaves, for example by installing host-exit-marker and reading ~/.dsh/logs/host-exit.log.

Why does adding a SIGHUP listener make the DeepSeek Harness host look hung?

DeepSeek Harness stops performing a signal's default action once the signal has a listener. A bare process.on('SIGHUP') turns termination into ignore, so the host no longer exits on hangup and looks stuck from outside. The correct approach is to write the record, remove the listener, and re-raise the signal against the process.

Why does a DeepSeek Harness signal-handler script that only writes a log exit immediately?

DeepSeek Harness signal handles are unref'd and do not keep the event loop alive. A script whose only pending work is waiting for a signal exits right after registering. A file-writing signal recorder must re-raise the signal after writing or explicitly stay alive; attaching a listener is not enough.

Does host-exit-marker fix DeepSeek Harness warning errors lost in the memory ring?

DeepSeek Harness host-exit-marker cannot fix warnings lost in the memory ring; it only writes a persistent record before and after process exit. Warning-level logs belong to the host log surface, which a plugin can neither read nor change. That half requires the host to mirror warnings to a rolling file under ~/.dsh/logs.

Related Terms

interrupt(0)
interrupt(0) is the exit path profile-boot takes after SIGTERM. It disposes resources, then exits the process with code 0 after the grace period, so a signal exit cannot be distinguished from a normal exit by code.— https://github.com/deepseek-ai/deepseek-harness/discussions/10022
host-exit-marker
host-exit-marker is the community plugin @argszero/dsh-host-exit-marker. It appends three kinds of records — start, signal, and died-without-exit — to ~/.dsh/logs/host-exit.log.— https://github.com/argszero/dsh-host-exit-marker
died-without-exit
died-without-exit is a record host-exit-marker writes on the next start. It uses the last-alive time from a heartbeat file to mark deaths that had no chance to run any handler.— https://github.com/argszero/dsh-host-exit-marker
warning ring (64 KB in-memory buffer)
The warning ring is the fixed-size memory buffer the desktop host main process uses for warning-level plugin errors. It is lost when the process exits and is never written to disk.— https://github.com/deepseek-ai/deepseek-harness/discussions/10022

Sources