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

@skwid138/opencode-command-normalizer

v0.2.0

Published

Command normalization plugin for opencode permission matching

Readme

opencode-command-normalizer

npm version CI License: MIT

Command normalization plugin for opencode permission matching.

Why

opencode expands configured permission patterns such as ~/tool.sh to absolute home paths when it loads configuration, but bash commands are matched against the raw command text. That means a command typed with a home-form argv0 can miss an otherwise anchored allow rule unless the config falls back to broad wildcard patterns.

This plugin rewrites only command-node argv0 home forms to the equivalent absolute path before opencode's permission matcher sees the command. It is meant to let anchored absolute permission rules work without unsafe leading wildcards.

Install

npm install @skwid138/opencode-command-normalizer

Register it in opencode using the singular plugin config key in tuple form:

{
  "plugin": [
    [
      "@skwid138/opencode-command-normalizer",
      {
        "roots": ["~/workspace/tools"],
        "expandBraceHome": false
      }
    ]
  ]
}

Install the bare package name. The package also exposes ./server for loaders that resolve that subpath, but user installation should use @skwid138/opencode-command-normalizer.

Configuration

Options:

| Option | Type | Default | Description | | --- | --- | --- | --- | | roots | string[] | unset | Optional blast-radius limiter. Expanded argv0 values outside configured roots are left unchanged. | | expandBraceHome | boolean | false | Enables ${HOME} and ${HOME}/... expansion at command-node starts. | | homedir | string | OS home directory | Test/advanced override for home expansion. | | auditLogPath | string | data-home audit log | Absolute path override for the NDJSON audit log. Relative paths are ignored. | | debugLogPath | string | data-home debug log | Absolute path override for debug messages. Relative paths are ignored. | | decisionsLogPath | string | data-home decisions log | Absolute path override for the NDJSON permission decision log. Relative paths are ignored. |

Default logs resolve lazily at plugin startup. If XDG_DATA_HOME is set to a truthy absolute path, logs live below that directory; otherwise they live below $HOME/.local/share. The subfolder intentionally keeps the historical name opencode/permission-audit-plugin for continuity, even though the package is now named command-normalizer.

auditLogPath, debugLogPath, and decisionsLogPath overrides must be absolute paths. Relative override values are ignored and the defaults are used instead, preserving the plugin's fail-open posture for malformed configuration.

How it works

The plugin hooks tool.execute.before for bash tool executions and mutates output.args.command before opencode permission matching. It expands only home forms in argv0 position at command-node starts:

  • ~
  • ~/...
  • $HOME
  • $HOME/...
  • optionally ${HOME} and ${HOME}/... when expandBraceHome: true

Command nodes are split at shell separators such as &&, ||, pipes, semicolons, ampersands, and newlines while respecting quotes. The plugin returns the original command unchanged for uncertain shell shapes such as heredocs, command substitution, arithmetic expansion, process substitution, unbalanced quotes, leading environment assignments, grouped commands, quoted argv0, and dot-dot argv0 values when roots are configured.

Decision capture

The plugin also listens to opencode's generic event hook for interactive permission prompts. It caches permission.asked events by the pair (event.properties.sessionID, event.properties.id), then joins the later permission.replied event by (event.properties.sessionID, event.properties.requestID). Joined decisions are appended as NDJSON to a separate decisions.log; the command-node audit.log schema is unchanged.

Decision records have this shape:

{
  ts: string;            // reply time
  sessionID: string;
  callID: string | null; // from permission.asked.properties.tool.callID
  requestID: string;
  permission: string;
  patterns: string[];
  always: string[];      // candidate patterns offered at ask time, independent of the chosen reply
  reply: unknown;        // recorded reply value; normally "once", "always", or "reject"
  askedTs: string;       // ask time
}

reply is normalized before writing so malformed runtime values cannot drop a decision record. JSON-serializable values are preserved verbatim. undefined, functions, symbols, and non-finite numbers are recorded as null in the NDJSON output. Values that fail JSON serialization, such as BigInt, circular objects, or throwing toJSON implementations, are coerced to a string; if string coercion also fails, the plugin records "[unserializable reply]".

Runtime event coverage:

| Permission path | Events emitted | Decision log entry | | --- | --- | --- | | Static allow | none | no | | Static deny | none | no | | Interactive ask | permission.asked + permission.replied | yes |

Use callID to join a decision record back to the command audit record emitted for the same bash tool invocation. If opencode omits the asked event's tool callID, the decision record uses callID: null and the plugin writes a debug line so the gap is visible.

Development

npm install
npm test
npm run build

npm run build emits declarations and verifies the public TypeScript surface.

Security

This package is a normalization aid, not a policy engine. It should only rewrite to the path the shell would produce for home expansion, and it intentionally bails out instead of guessing on ambiguous shell syntax. See SECURITY.md for the command-rewrite scope and fail-open philosophy.

License

MIT