npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@argszero/dsh-histexpand-guard

v0.1.1

Published

Refuses a bash command before it runs when the persistent shell would drop it to history expansion. The persistent bash tool writes each command into an interactive shell as one $'…' line, and an interactive bash expands history (`!…`) before parsing anyt

Readme

@argszero/dsh-histexpand-guard

Refuses a bash call before it runs when the persistent shell would drop the line to history expansion — the command never executes, nothing is asked of the operator, and the model is told why instead of waiting five minutes for a deadline.

It refuses exactly that, and nothing else:

  • a command with a single quote and a ! after it — the point where the wrapper's $'…' quoting stops protecting the command text;
  • where that ! is one an interactive bash would act on: not preceded by a backslash, not followed by a space or =, and not inside a comment the expander honours (a preceding space, ; or &).

Both are facts about the text. The rule was measured, not reasoned: scripts/calibrate-history-expansion.py builds the exact line the wrapper builds, feeds it to a real interactive bash, and compares the verdict — 45/45 agreement on the pinned corpus at the time of writing.

The problem

The persistent bash tool does not run a command the way a script would (#9240, #5046). It renders the command into one physical line of an interactive shell and writes that line to the shell's stdin:

printf '%s\n' $'<START>'; eval -- $'<command>'; st=$?; printf '%s%s\n' $'<END>' "$st"

History expansion runs on that line before bash parses anything, and an interactive shell has histexpand on by default. When the expander reaches a ! it cannot resolve it prints bash: !word: event not found and discards the whole line — the eval, the exit-status capture and the closing printf included. The tool then polls a scrollback that will never contain its end marker until timeoutMs expires: 300 seconds, the shipped default.

So a command like this one hangs the session for five minutes and produces no output and no reason:

cat > x.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
EOF

It looks ordinary, and that is the point — the reports arrive as timeout mysteries. Two measurements show the shape of it:

echo "hi!"     # runs
echo 'hi!'     # hangs

The difference is not in the command's meaning. The wrapper renders a caller's ' as \' inside its $'…' region, and the expander reads that as the end of the region without honouring the backslash — so from the first single quote onward the command text is no longer inside the wrapper's quoting, and a ! after it reaches history expansion. A ! before any single quote is still protected, which is why echo "!" 'a' runs.

The fix is upstream, and it is one line

Next to the stty -echo the persistent tool already sends at shell initialization:

set +o histexpand

That removes the hazard for every command, including the shapes no rule can classify, and it is the change this plugin exists to make unnecessary. A plugin cannot make it — the shell is created inside the tool's own body — so this package does the two things a plugin can do: it refuses the shape it can prove, and arm: true writes that same line into the agent's live shell for an operator who wants the complete fix without patching upstream.

Install

npm install @argszero/[email protected]

The package ships a bundle patch (dsh.bundle.patch: ./cordis.patch.yml), so a deployment that follows the harness's bundle convention gets it by adding one entry:

- insert:
    - id: histexpand-guard
      name: '@argszero/dsh-histexpand-guard'

Mounting it directly works the same way and takes the same config:

import guard from '@argszero/dsh-histexpand-guard'

ctx.plugin(guard, { mode: 'report' })

Configuration

| key | default | meaning | |---|---|---| | mode | 'enforce' | enforce refuses the call; report logs the same finding and lets the call run — the way to measure this guard against a shell it was not calibrated on; off installs nothing at all | | onlyTools | ['bash'] | the tool names to inspect. An empty list inspects every tool with a non-empty command argument | | assumePersistentShell | false | act on a command argument without first resolving the tool to the persistent shell's shape. For a deployment that renamed or wrapped its shell tool | | arm | false | also write set +o histexpand into the agent's live persistent shell, once per session. Requires a terminals provider; silently inert without one | | exempt | [] | commands containing any of these substrings are never refused | | tool | true | register the histexpand_check tool | | maxReasonChars | 1600 | inclusive bound for the denial text | | maxReportedDesignators | 3 | inclusive bound on how many designators one denial names |

report is not decoration. This guard's rule is a measurement against a particular bash, so an operator on a different build is right to want to watch it before believing it:

ctx.plugin(guard, { mode: 'report' })

The check tool

histexpand_check answers the same question on demand, without running anything — the way to know a command is safe before the shell tells you it is not:

histexpand_check({ command: "cat > x.sh <<'EOF'\n#!/usr/bin/env bash\nEOF" })

It reports whether the guard would refuse, which tool it resolved and therefore whether it would act at all, and the exact $'…' line the wrapper would send.

Why it will not refuse the wrong tool

@deepseek-ai/dsh-tool-bash and @deepseek-ai/dsh-tool-bash-persistent both register a tool named bash, and only the persistent one has this failure mode: the one-shot tool spawns bash -c, a non-interactive shell, where histexpand is off and a ! is ordinary text. Refusing a call served by the one-shot tool would break commands that work — the exact harm this guard exists to avoid.

The registry refuses a duplicate name within one layer, and scoped registrations shadow globals, so for any running profile the name bash resolves to exactly one definition. The guard reads that definition with ctx.tools.get(name, agent) — the same lookup the registry performs before dispatch — and requires the persistent tool's compiled shape:

| resolved definition | tool-bash (one-shot) | tool-bash-persistent | |---|---|---| | parameters.properties | description, command, timeoutMs, workdir, … | command alone | | parameters.required | description, command | command alone | | output.schema.type | (none — a oneOf of result kinds) | "string" |

The check is structural, not by description text, because the persistent tool's description is deployment-configurable while its shape is what its defineTool call fixes. It reads the compiled JSON Schema the registry hands out, not the parameter map the package's source shows. A deployment that knows its provider sets assumePersistentShell.

What it deliberately does not do

  • It does not judge commands, and it does not look at anything but the command argument.
  • It does not act on a tool it cannot identify: an unresolvable definition, an unreadable one, or one whose shape changed upstream is not the persistent shell as far as this guard is concerned.
  • It does not touch the shell unless arm is set.
  • It fails open: a scan that throws is logged and the call proceeds. A guard whose own bug refuses every call in a deployment is a worse failure than the one it prevents.

Every deliberate looseness in the scan leans the same way: toward not reporting a designator. A missed designator leaves the deployment exactly as it is today; a spurious one refuses a command that works.

Compatibility

Verified against the 0.1.7-rc.2 and 0.2.0-rc.2 lines. The peer range is quoted here in full, because a README that quotes only its first || segment is a trap:

@deepseek-ai/dsh-tools: >=0.1.7-rc.2 <0.2.0 || >=0.2.0-rc.2 <0.2.0

Both lines are pinned by npm run test:probe-lines, which installs the whole line, builds, and runs the suite against it. Zero runtime dependencies.

Running the checks

npm test                        # tsc, then the behaviour suite through a real ToolRuntime
python3 scripts/calibrate-history-expansion.py   # the rule against a real interactive bash (45/45)
node scripts/probe-bang-matrix.py                # the raw bash behaviour the rule encodes
node scripts/inject-defects.mjs # mutate the source; every arm must be caught by the suite
npm run test:probe-lines        # every peer line the manifest admits

test/guard.spec.mjs drives the shipped pipeline, not a mock: a real cordis context, the real ToolRuntime, real tool definitions, and only the tool body fabricated. Every refusal is paired with a control arm — the same command with the plugin absent, which must run — so "the call failed" means "this plugin refused it" rather than "the registry failed".

License

MIT