@wyattjoh/demur
A proof-of-concept destructive-command guard for coding agents.
Package details
Install @wyattjoh/demur from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@wyattjoh/demur- Package
@wyattjoh/demur- Version
0.4.2- Published
- Sep 20, 2026
- Downloads
- 915/mo · 915/wk
- Author
- wyattjoh
- License
- MIT
- Types
- extension
- Size
- 114.4 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
demur
A proof-of-concept harmful-command guard for coding agents. demur sends a shell
command and limited execution context to TypeSafe System One, then turns six
model judgments into an allow, ask, or deny decision.
[!WARNING] demur is experimental and is not a security boundary. A model can misclassify, behave nondeterministically, or be influenced by attacker-controlled command text. Use it as an additional confirmation layer, not as your only protection against harmful commands.
How it works
For each agent-initiated Bash tool call, demur:
- Collects the command, working directory, host name, and bounded Git facts.
- Requests six judgments in one TypeSafe System One call:
- whether the command executes a destructive operation;
- whether it exposes secrets, credentials, or personal data;
- whether it weakens a security boundary or grants elevated access;
- whether its effects are recoverable;
- whether it targets shared infrastructure; and
- its expected blast radius.
- Applies deterministic thresholds from
src/policy.ts. - Escalates an otherwise allowed destructive command to
askwhen variables, globs, or command substitutions make its real target statically uncertain. - Maps the decision into Pi or Claude Code's permission protocol.
Environment access, Git queries, and network calls are Effect services. The
policy and shell analysis remain pure functions, while src/guard.ts exposes a
Promise boundary for host integrations.
Data disclosure
Every judged command makes a request to TypeSafe. demur sends:
- the complete command string;
- the working directory;
- the requesting host (
piorclaude-code); - the repository root and current branch, when inside Git; and
- counts of modified, untracked, and unpushed changes plus whether an upstream branch exists.
demur does not send file contents, environment-variable values, remote URLs, or its static-analysis result. Command strings and paths can still contain secrets or sensitive names. Review TypeSafe's service terms and data-handling policy before enabling demur in a sensitive repository. Do not run secrets directly in shell arguments when the guard is active.
Requirements
- Bun 1.4 or newer
- A TypeSafe System One API key from https://console.typesafe.ai/settings/keys
- Network access to TypeSafe for every judged command
- Pi 0.85.x and/or Claude Code
Install
Install demur's command-line tools and save your TypeSafe API key in the operating system credential store:
bun add --global @wyattjoh/demur
demur auth login
demur auth status
Bun.secrets stores the credential in macOS Keychain, Linux Secret Service, or
Windows Credential Manager. The operating system may request access when the
credential is first used or while its credential store is locked.
For automation or a one-off override, set TYPESAFE_API_KEY before launching
the host agent. An environment value takes precedence over the stored key:
export TYPESAFE_API_KEY="..."
Never commit the key. .env.schema documents the accepted
environment configuration, and local environment files are ignored by Git.
Pi
Install the npm package:
pi install npm:@wyattjoh/demur
Pin a specific release when reproducibility matters:
pi install npm:@wyattjoh/demur@0.4.2
Launch Pi normally after configuring the credential:
pi
The extension intercepts bash tool calls. Because Pi runs extensions under
Node.js while demur uses Bun.secrets, the extension launches a package-local
Bun worker for each judgment. The API key remains inside that worker; only the
command request and resulting verdict cross its local stdio pipes. ask opens
an interactive confirmation dialog; without an interactive UI, demur blocks the
command.
Use /demur to open the extension menu. It can enable or disable demur and
change what Pi does when demur cannot obtain a trustworthy judgment because of
a missing credential, timeout, API error, malformed worker response, or
unexpected guard failure:
block(default) fails closed.askrequests interactive confirmation and blocks when no UI is available.allowfails open without confirmation.
Disabling demur bypasses the worker and allows Bash calls without judgment. Pi's
bottom status bar always shows demur: enabled or demur: disabled so this
bypass remains visible.
Both settings are stored globally at $XDG_CONFIG_HOME/demur/config.json, or
~/.config/demur/config.json when XDG_CONFIG_HOME is unset, and apply to
future Pi sessions. While demur is enabled, the failure policy never changes a
completed deny policy judgment; those commands remain blocked.
After each run, Pi's interactive UI prints the decision, submitted input-token count, the run's estimated input cost, the accumulated global estimate, and the wall-clock evaluation time in human-readable units. The estimate uses TypeSafe's published Jev price of $0.042 per million input tokens; it is informational rather than an authoritative billing amount. Failure and bypass paths that do not call Jev report that cost is unavailable.
The accumulated estimate is stored at $XDG_STATE_HOME/demur/usage.json, or
~/.local/state/demur/usage.json when XDG_STATE_HOME is unset. A lock
serializes concurrent Pi instances, and each update is written to a temporary
file before an atomic rename so the total cannot be partially written or lose a
concurrent increment. Cost-accounting failures do not change demur's guard
decision; the status reports accumulated unavailable instead.
Pi packages execute with the user's full system permissions. Review this repository before installing it.
For development, clone the repository and install it by local path:
git clone https://github.com/wyattjoh/demur.git
cd demur
bun install --frozen-lockfile
pi install "$PWD"
Claude Code
The global package installation above also provides the Claude Code hook.
Register its executable in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "demur-claude-hook"
}
]
}
]
}
}
Launch Claude Code normally after configuring the credential. The adapter emits
Claude Code's hookSpecificOutput.permissionDecision response.
CLI
Manage the stored credential or judge a single command without a host integration:
demur auth login
demur auth status
demur auth logout
demur judge "git reset --hard HEAD~3"
From a development checkout, bun run judge "<command>" remains available.
Configuration
| Variable | Default | Purpose |
|---|---|---|
TYPESAFE_API_KEY |
stored credential | Optional TypeSafe API credential override. Missing keys fail closed. |
DEMUR_TIMEOUT_MS |
4000 |
Per-attempt model timeout in milliseconds. |
DEMUR_DISABLE |
unset | Emergency bypass. 1 or true allows every command. |
Failure posture
demur's core guard fails closed. A missing key, credential-store failure,
timeout, API failure, malformed response, or unexpected guard error returns
deny with a reason that identifies the guard failure rather than presenting it
as a policy judgment. The Claude Code adapter and CLI preserve that verdict.
The Pi extension defaults to enabled with the same fail-closed behavior, but
its explicit /demur menu can globally change how Pi handles guard failures or
disable the extension entirely. The failure-policy override applies only when
no trustworthy judgment was produced; it cannot loosen a completed policy
denial while demur is enabled. The bottom status bar makes the enabled state
visible.
DEMUR_DISABLE=1 remains the cross-host emergency bypass. It disables judgment
and protection entirely and should remain unset during normal use.
Known limitations
Bun.secretsis experimental, and credential-store availability and prompts vary by operating system configuration.- Model decisions are probabilistic and may vary between identical requests.
- The hard-coded
jev-latestmodel alias may change without a demur release. - Attacker-controlled command text can influence the model.
- Shell expansion, obfuscation, aliases, wrappers, and runtime environment can make a command behave differently from its text.
- Network outages block commands by default; Pi can override that failure
handling from the
/demurmenu. - Every decision adds remote-call latency and may incur provider cost.
- The integrations guard agent-issued Bash tool calls only. They do not guard user shells, other process-launching tools, or commands run outside the host.
- Other Pi extensions loaded after demur can mutate a tool call after it has been judged.
Use operating-system permissions, backups, repository protections, sandboxing, and deterministic policy controls alongside demur.
Project layout
src/questions.ts— the six model judgmentssrc/policy.ts— thresholds andallow/ask/denycompositionsrc/analyze.ts— deterministic shell analysis for the static uncertainty gatesrc/state.ts— bounded environment and Git context collectionsrc/key.ts— environment precedence and operating-system credential storagesrc/guard.internal.ts— Effect-native orchestration and fail-closed recoverysrc/guard.ts— managed runtime and Promise boundaryextensions/demur/— Pitool_callintegrationsrc/adapters/claude-code.ts— Claude CodePreToolUseintegrationeval/— safe synthetic contrast cases and the live evaluation runner
Synthetic evaluation
The synthetic evaluation measures whether the two policy-qualification questions separate clear positive and negative cases, then verifies that active hazards produce a policy denial. Its 60 commands are hand-authored fixture strings with synthetic names and no secret values. The runner never executes a candidate command. It only sends each string and fixed synthetic context to TypeSafe.
Run one sample per case:
bun run eval:synthetic
Repeat each case (up to 10 samples) to expose model instability:
bun run eval:synthetic --runs=3
The command prints each case's expected and observed classification, exits
nonzero on a miss or provider failure, and writes full evidence to
.scratch/synthetic-eval.json. Repeated runs make additional provider calls and
may incur cost, so the live evaluation is deliberately not part of bun run ci.
Development
bun install --frozen-lockfile
bun run check
bun run test
bun run build
All three checks run together with bun run ci.
See CONTRIBUTING.md before proposing changes and RELEASING.md for the automated release process. Report security issues through SECURITY.md, not a public issue.