SLOPSHOPPER

blast-radius

See what a risky command would change before it runs.

newpanebandguardprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · blast-radius
│ ┃ Blast Radius ✕ › fix the failing auth test and add an audit log call │ ┃ Blast Radius: rm -rf build && git push │ ┃ --force origin main ⏺ Read(src/auth.ts) │ ┃ Would delete 3 file(s) () under build. ⎿ Read 6 lines │ ┃ package.json ⏺ Update(src/auth.ts) │ ┃ README.md ⎿ Added 2 lines, removed 1 line │ ┃ src ⏺ Bash(bun test) │ ┃ [ Proceed ] [ Cancel ] ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Blast Radius
Blast Radius: rm -rf build && git push --force origin main Would delete 3 file(s) () under build. package.json README.md src [ Proceed ] [ Cancel ]
README

DeepSeek Harness

English | 中文

DeepSeek Harness (dsh) is an open-source agent harness developed by DeepSeek AI.

It is built on an everything-is-a-plugin architecture and powered by Cordis, whose design is described in _A Programming Paradigm for Spatiotemporal Composability_.

Documentation: https://deepseek-harness.github.io/deepseek-harness/

Developer preview

DeepSeek Harness is in developer preview and iterating rapidly. THERE WILL BE COMPATIBILITY-BREAKING CHANGES.

Review the safety notice before running the project.

Run

Run from npm

Install Node.js, then run:

npx @deepseek-ai/dsh web

The command starts the Web UI at http://127.0.0.1:3080 by default and opens it in the default browser for a local launch. An SSH launch only prints the host URL because the SSH client or editor owns the local forwarded address. Pass --no-open to run the server without opening a browser. See Web UI guide.

Run from source

To run from a repository checkout:

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web

pnpm run build prepares the repository artifacts. pnpm dsh web uses those built artifacts without rebuilding.

Community and support

  • Submit feedback or bug reports through GitHub Discussions.
  • Add the dsh-plugin topic to your plugin repository for discoverability.
  • Join <a href="https://discord.gg/4MrtZUhpxg">DeepSeek Harness Discord community</a>.

Contributing

See CONTRIBUTING.md.

Development

Start with the development guide and architecture documentation.

pnpm run dev:web builds, serves, and rebuilds client bundles on source edits in one terminal, and make help lists the matching Make targets for Web and Desktop; the guide's application commands section owns the full table.

For agents, follow AGENTS.md.

Citation

@misc{deepseek-harness2026,
  title={DeepSeek Harness: Everything is a Plugin},
  author={DeepSeek-AI},
  year={2026},
  publisher={GitHub},
  howpublished={\url{https://github.com/deepseek-ai/deepseek-harness}},
}

License

MIT

Third-party dependencies and their licenses are disclosed in THIRD_PARTY_NOTICES.md.

Source 1 files
hooks/blast-radius.mjs 128 lines
1// Blast Radius, from "Getting started with Claude Code mods"
2// (https://claude.dev/blog/getting-started-with-claude-code-mods/, Anthropic, 2026-10-01).
3// The tool.call hook adds a waitArgv option for DSH's portable timer; the rest of the module
4// (classify, measure, the pane and band trees) is completed to the post's
5// description of the mod, which is marked where it starts.
6
7// Blast Radius: see what a risky command would change before it runs.
8
9// —— completed to the post's description (not in the published excerpt) ——
10
11const RISKS = [
12  { pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b/, kind: "delete", what: "delete files recursively" },
13  { pattern: /\bgit\s+reset\s+--hard\b/, kind: "reset", what: "discard uncommitted changes" },
14  { pattern: /\bgit\s+clean\b/, kind: "clean", what: "delete untracked files" },
15  { pattern: /\bgit\s+push\b[^\n]*\s(--force|-f)\b/, kind: "force-push", what: "rewrite a remote branch" },
16  { pattern: /\b(migrate|manage\.py\s+migrate|prisma\s+migrate|rails\s+db:migrate)\b/, kind: "migrate", what: "change the database schema" },
17];
18
19// What the hook is holding: one command at a time, until a button decides.
20let held = null;
21
22export function register(on, { waitArgv = ["sleep", "0.25"] } = {}) {
23  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
24    const risk = classify(String(e.command ?? ""));
25    if (risk === null) return next(e);                 // everything else runs as normal
26
27    const report = await measure($, risk, await $.session.cwd());  // git status, git clean -n, du, ...
28    held = { command: e.command, risk, report, decision: null };
29    const opened = await $.ui.open({ id: "blast-radius", title: "Blast Radius", focus: true });
30    if (!opened.isPlaced) held.where = "band";         // too narrow for a pane: draw above the prompt
31
32    while (held.decision === null && !next.signal.aborted) {
33      await $.process.run(waitArgv);                   // time inside $ calls doesn't count against the hook's time limit
34    }
35    if (held.decision === "proceed") return next(e);   // let it run
36    return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
37  });
38
39  // —— completed to the post's description (not in the published excerpt) ——
40
41  on("ui.render", { component: "Pane" }, ($, e, next) => {
42    if (e.requestId !== "blast-radius" || held === null || held.decision !== null) return next(e);
43    return report($, e);
44  });
45
46  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
47    if (held === null || held.decision !== null || held.where !== "band") return next(e);
48    return report($, e);
49  });
50}
51
52function classify(command) {
53  const risk = RISKS.find((r) => r.pattern.test(command));
54  return risk ? { kind: risk.kind, what: risk.what, command } : null;
55}
56
57async function measure($, risk, cwd) {
58  const lines = [];
59  let summary = `${risk.what} in ${cwd}`;
60  if (risk.kind === "delete") {
61    const target = risk.command.match(/\brm\s+-\S+\s+(\S+)/)?.[1];
62    const du = await run($, ["du", "-sh", target ?? "."]);
63    const size = du.stdout.trim().split(/\s+/)[0] ?? "?";
64    const count = await run($, ["find", target ?? ".", "-type", "f"]);
65    const files = count.stdout.split("\n").filter(Boolean).length;
66    summary = `delete ${files} file(s) (${size}) under ${target ?? cwd}`;
67    lines.push(...count.stdout.split("\n").filter(Boolean).slice(0, 9));
68  } else if (risk.kind === "reset") {
69    const status = await run($, ["git", "status", "--porcelain"]);
70    const changed = status.stdout.split("\n").filter(Boolean);
71    summary = `discard uncommitted changes to ${changed.length} file(s)`;
72    lines.push(...changed.slice(0, 9));
73  } else if (risk.kind === "clean") {
74    const dry = await run($, ["git", "clean", "-n"]);
75    const removed = dry.stdout.split("\n").filter(Boolean);
76    summary = `delete ${removed.length} untracked path(s)`;
77    lines.push(...removed.slice(0, 9));
78  } else if (risk.kind === "force-push") {
79    const ahead = await run($, ["git", "log", "--oneline", "HEAD..origin/main"]);
80    const commits = ahead.stdout.split("\n").filter(Boolean);
81    summary = `rewrite the remote branch, dropping ${commits.length} commit(s) not on this branch`;
82    lines.push(...commits.slice(0, 9));
83  } else if (risk.kind === "migrate") {
84    const pending = await run($, ["sh", "-c", "python manage.py showmigrations --plan 2>/dev/null | grep '\\[ \\]' || true"]);
85    const migrations = pending.stdout.split("\n").filter(Boolean);
86    summary = `apply ${migrations.length} pending migration(s)`;
87    lines.push(...migrations.slice(0, 9));
88  }
89  return { summary, lines };
90}
91
92// A dry run that fails (no git, no python, no such path) still lets the user decide.
93async function run($, argv) {
94  try {
95    return await $.process.run(argv);
96  } catch {
97    return { exitCode: 1, stdout: "", stderr: "" };
98  }
99}
100
101function decide(decision) {
102  if (held !== null) held.decision = decision;
103}
104
105function report($, e) {
106  const { Box, Text, Button } = $.ui.resolve(e);
107  const detail = held.report.lines.map((line) => Text({ dimColor: true, children: `  ${line}` }));
108  return Box({
109    flexDirection: "column",
110    border: true,
111    borderColor: "yellow",
112    paddingX: 1,
113    children: [
114      Text({ color: "yellow", bold: true, children: `Blast Radius: ${held.command}` }),
115      Text({ children: `Would ${held.report.summary}.` }),
116      ...detail,
117      Box({
118        flexDirection: "row",
119        gap: 2,
120        children: [
121          Button({ label: "Proceed", hotkey: "1", onPress: () => decide("proceed") }),
122          Button({ label: "Cancel", hotkey: "2", onPress: () => decide("cancel") }),
123        ],
124      }),
125    ],
126  });
127}
128