fix(defender): give each Defender plugin its own daemon - #39
Conversation
stackone-defender and stackone-defender-antigravity both used ~/.claude/defender.sock and the same state, lock and log files. They pin different Defender versions (0.8.2 vs 0.7.0), and each client replaces any daemon that doesn't match its own tree, so with both installed every scan killed the other plugin's daemon and cold-started its own. The paths now live in scripts/daemon-paths.mjs, imported by the hook client and the daemon so the two can't disagree. The Claude Code plugin keeps its existing file names, so current installs see no change. Antigravity moves to defender-antigravity.*. Its README no longer promises a shared daemon. Measured with both plugins alternating over 6 scans in a clean HOME: main: 6 daemon spawns, 5 kills, 0.8s per scan after warm-up fixed: 2 spawns, 0 kills, 0.4s per scan, both daemons reused Cold start on an M-series Mac was 0.8-2.5s, well under the 5s scan budget. That lowers the fail-open risk raised in #37, but slower machines may differ. Cost: two model copies in memory when both plugins are installed. Fixes #37 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="plugins/security/stackone-defender/scripts/daemon-paths.mjs">
<violation number="1" location="plugins/security/stackone-defender/scripts/daemon-paths.mjs:13">
P3: This entire script set is vendored twice: `plugins/security/stackone-defender-antigravity/scripts/daemon-paths.mjs` is byte-identical to this file except for `NAME = "defender-antigravity"`, and the two `scan-tool-result.mjs`/`defender-daemon.mjs` copies are likewise duplicated. Every future change to the daemon client protocol, path layout, or log handling must now be applied in two trees, and a third Defender variant would need a third copy. Consider generating `NAME` from the plugin's package identity and sharing the scripts between the two plugin trees at packaging time (or a script that syncs them) so the paths can't drift.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| import { join } from "path"; | ||
|
|
||
| const DIR = join(homedir(), ".claude"); | ||
| const NAME = "defender"; |
There was a problem hiding this comment.
P3: This entire script set is vendored twice: plugins/security/stackone-defender-antigravity/scripts/daemon-paths.mjs is byte-identical to this file except for NAME = "defender-antigravity", and the two scan-tool-result.mjs/defender-daemon.mjs copies are likewise duplicated. Every future change to the daemon client protocol, path layout, or log handling must now be applied in two trees, and a third Defender variant would need a third copy. Consider generating NAME from the plugin's package identity and sharing the scripts between the two plugin trees at packaging time (or a script that syncs them) so the paths can't drift.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/security/stackone-defender/scripts/daemon-paths.mjs, line 13:
<comment>This entire script set is vendored twice: `plugins/security/stackone-defender-antigravity/scripts/daemon-paths.mjs` is byte-identical to this file except for `NAME = "defender-antigravity"`, and the two `scan-tool-result.mjs`/`defender-daemon.mjs` copies are likewise duplicated. Every future change to the daemon client protocol, path layout, or log handling must now be applied in two trees, and a third Defender variant would need a third copy. Consider generating `NAME` from the plugin's package identity and sharing the scripts between the two plugin trees at packaging time (or a script that syncs them) so the paths can't drift.</comment>
<file context>
@@ -0,0 +1,20 @@
+import { join } from "path";
+
+const DIR = join(homedir(), ".claude");
+const NAME = "defender";
+
+export const SOCKET_PATH = join(DIR, `${NAME}.sock`);
</file context>
There was a problem hiding this comment.
Fair point, but I'm keeping it out of this PR.
The duplication predates this change. The two plugins already carry separate copies of scan-tool-result.mjs, defender-daemon.mjs and deps-fingerprint.mjs, because each is installed on its own into a different host with its own node_modules. daemon-paths.mjs follows that existing layout, and the only intended difference between the two copies is NAME.
Sharing the scripts at packaging time, or adding a sync check, is worth doing, but it's a change to how both plugins are built. It should be reviewed on its own rather than folded into a bug fix.
…ty README Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: This PR changes daemon path management to per-plugin paths: approximates correct behavior but contains inconsistencies that may break daemon initialization.
Re-trigger cubic
Summary
Fixes #37. The two Defender plugins shared one background daemon, so installing both made each one kill the other's daemon on every scan.
scripts/daemon-paths.mjsper plugin. The hook client and the daemon both import it, so they always agree on the socket and state files.~/.claude/defender.*), so existing installs see no change.~/.claude/defender-antigravity.*. Its README no longer promises a shared daemon.Evidence
Both plugins alternating over 6 scans, in a clean
HOME:mainCold start measured 0.8 to 2.5s on an M-series Mac, well under the 5s scan budget. So the fail-open risk in #37 looks smaller than estimated, though slower machines may differ.
Trade-off
With both plugins installed there are now two daemons and two copies of the model in memory. The alternative was aligning versions so one daemon could serve both, but the dependency stamp added in #36 differs between the plugins anyway.
Test plan
npm testin both plugins: 15 and 12 passmainreproduces the thrash🤖 Generated with Claude Code
Summary by cubic
Gives each Defender plugin its own background daemon so installing both no longer makes them kill each other's daemon on every scan.
stackone-defenderandstackone-defender-antigravitypreviously shared~/.claude/defender.sockplus the same state, lock, and log files, but they pin different Defender versions and each client replaces any daemon that doesn't match its own dependency tree.The socket and state paths now live in a new
scripts/daemon-paths.mjsper plugin, imported by both the hook client and the daemon so they can't disagree. The Claude Code plugin keeps its existing~/.claude/defender.*paths, so current installs see no change; Antigravity moves to~/.claude/defender-antigravity.*and its README no longer claims a shared daemon.Trade-off
Benchmarks
Written for commit 42540c4. Summary will update on new commits.