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

@0xkobold/pi-whitelist

v0.5.2

Published

Tri-state tool permission system (allow/deny/ask) for AI agent tool invocations

Readme

pi-whitelist

Tri-state tool permission system (allow/deny/ask) for AI agent tool invocations

A standalone, runtime-agnostic npm module for controlling AI agent tool invocations.

Why?

Every AI agent that executes tools needs permission gating. Without it, agents can run destructive commands without oversight. pi-whitelist gives you a simple, battle-tested model:

| # | Action | Behavior | |---|---|---| | 1 | Allow once | Let it run this time, don't persist | | 2 | Allow always | Save a rule, auto-approve future matches | | 3 | Deny | Block the invocation |

Every check returns one of three decisions: allow, deny, or ask. No booleans, no ambiguity.

Install

npm install @0xkobold/pi-whitelist

Quick Start

import { PermissionManager } from '@0xkobold/pi-whitelist'

const manager = new PermissionManager()

// Read-only tools are auto-allowed
manager.check({ toolName: 'Read' })
// → { behavior: 'allow', decisionReason: { type: 'other', reason: 'read-only-tool' } }

// Destructive tools require approval
manager.check({ toolName: 'Bash', ruleContent: 'docker build .' })
// → { behavior: 'ask', message: 'Bash: docker build .', suggestions: [...] }

// Add an "allow always" rule
manager.addRule({ toolName: 'Bash', ruleContent: 'git *' }, 'allow', 'session')
manager.check({ toolName: 'Bash', ruleContent: 'git status' })
// → { behavior: 'allow', decisionReason: { type: 'rule', rule: ... } }

// Add a deny rule — deny always wins over allow for the same pattern
manager.addRule({ toolName: 'Bash', ruleContent: 'rm -rf *' }, 'deny', 'session')
manager.check({ toolName: 'Bash', ruleContent: 'rm -rf /tmp' })
// → { behavior: 'deny', message: 'Permission denied for Bash: rm -rf /tmp', ... }

Three-State UI Integration

const decision = manager.check({ toolName: 'Bash', ruleContent: 'docker build -t myapp .' })

if (decision.behavior === 'ask') {
  // Show the prompt:
  // ┌──────────────────────────────────────────────┐
  // │  Bash: docker build -t myapp .                │
  // │                                                │
  // │  1. Allow          (allow this once)           │
  // │  2. Allow always   (add rule to settings)      │
  // │  3. Deny           (block this invocation)     │
  // └──────────────────────────────────────────────┘
  const userChoice = await showPermissionPrompt(decision)

  if (userChoice === 1) {
    // Allow once — no persistence
    return { behavior: 'allow', decisionReason: { type: 'other', reason: 'user-allow-once' } }
  }
  if (userChoice === 2) {
    // Allow always — persist the rule
    manager.addRule(
      { toolName: 'Bash', ruleContent: 'docker *' },
      'allow',
      'projectSettings', // persists to .pi/settings.json
    )
  }
  if (userChoice === 3) {
    // Deny
    return { behavior: 'deny', message: decision.message, ... }
  }
}

Rule Format

Rules use the format ToolName or ToolName(content) with glob patterns:

| Rule | Matches | |---|---| | Bash | Any Bash invocation | | Bash(git *) | Any command starting with git | | Bash(npm test) | Exactly npm test | | FileEdit(/src/**) | Any file edit under /src/ | | Read | The Read tool (no content filter) | | Bash(python -c "print\(1\)") | Commands with escaped parentheses |

Permission Modes

| Mode | Behavior | |---|---| | default | Prompt for destructive tools, auto-allow read-only tools | | bypassPermissions | Allow everything — dangerous, for CI/testing only | | plan | Ask for everything, even read-only tools | | acceptEdits | Auto-allow file edits, ask for everything else | | dontAsk | Allow everything not explicitly denied |

const ci = new PermissionManager({ mode: 'bypassPermissions' })
ci.check({ toolName: 'Bash', ruleContent: 'rm -rf /' })
// → { behavior: 'allow', decisionReason: { type: 'mode', mode: 'bypassPermissions' } }

Rule Sources & Priority

Rules come from multiple sources, merged by priority (lowest → highest):

| Source | File | Priority | |---|---|---| | userSettings | ~/.pi/settings.json | 0 (lowest) | | projectSettings | .pi/settings.json | 1 | | localSettings | .pi/settings.local.json | 2 | | flagSettings | CLI flags | 3 | | policySettings | Enterprise/org policy | 4 | | cliArg | --allowedTools | 5 | | command | /command in session | 6 | | session | In-memory only | 7 (highest) |

Deny always wins over allow when both match the same tool+content.

Settings File

.pi/settings.json:

{
  "permissions": {
    "defaultMode": "default",
    "allow": [
      "Bash(git *)",
      "Bash(npm test)",
      "FileEdit(/src/**)",
      "Read"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(sudo *)"
    ],
    "ask": [
      "Bash(docker *)"
    ],
    "additionalDirectories": []
  }
}

API Reference

PermissionManager

The main class. Holds rules, resolves checks, persists changes.

const manager = new PermissionManager({
  store?: SettingsStore,              // Storage backend (default: MemorySettingsStore)
  mode?: PermissionMode,              // 'default' | 'bypassPermissions' | 'plan' | 'acceptEdits' | 'dontAsk'
  additionalWorkingDirectories?: Map<string, WorkingDirectorySource>,
  isBypassPermissionsModeAvailable?: boolean,
  shouldAvoidPermissionPrompts?: boolean,
})

| Method | Description | |---|---| | check(input) | Evaluate a tool invocation against all rules | | addRule(rule, behavior, source) | Add a rule and persist it | | removeRule(rule, behavior, source) | Remove a matching rule | | setMode(mode) | Switch permission mode | | applyUpdates(updates) | Batch-apply permission updates | | isBashAllowed(command) | Convenience: check if Bash command is allowed | | isFileEditAllowed(path) | Convenience: check if file path edit is allowed | | getRulesForTool(toolName) | Get all rules for a specific tool | | getRulesFromSource(source) | Get all rules from a specific source | | getContext() | Get the full permission context snapshot | | invalidateCache() | Clear the rule evaluation cache |

checkPermission(input)

Standalone one-shot check with default settings:

import { checkPermission } from '@0xkobold/pi-whitelist'

const decision = checkPermission({ toolName: 'Read' })
// → { behavior: 'allow', ... }

Matchers

| Matcher | For | Behavior | |---|---|---| | GlobMatcher | Default fallback | Uses picomatch for glob matching | | CommandMatcher | Bash, PowerShell | Splits on &&, \|\|, ;, \| and checks prefix | | FileMatcher | FileEdit, FileWrite | Normalizes paths to POSIX, uses picomatch |

Storage

| Store | Use | |---|---| | MemorySettingsStore | Testing, ephemeral sessions | | FileSettingsStore | Persistent settings in .pi/settings.json |

import { MemorySettingsStore, FileSettingsStore, mergeSettings } from '@0xkobold/pi-whitelist'

const mem = new MemorySettingsStore({ permissions: { allow: ['Bash(git *)'], deny: [], ask: [], additionalDirectories: [] } })
const file = new FileSettingsStore('.pi/settings.json')
await file.load()

Read-Only Tools

These tools auto-allow in default mode without prompting:

Read, FileRead, Glob, Grep, WebFetch, WebSearch, TaskGet, TaskList, TaskOutput, ListMcpResources, ReadMcpResource, ToolSearch, LSP, AskUser

Error Classes

| Error | Code | When | |---|---|---| | PermissionError | Various | Base error class | | RuleParseError | RULE_PARSE_ERROR | Malformed rule string | | StorageError | STORAGE_ERROR | File read/write failure | | MatcherError | MATCHER_ERROR | Glob/command matching failure |

Subpath Exports

import { parseRuleString, serializeRuleString } from '@0xkobold/pi-whitelist/rules'
import { GlobMatcher, CommandMatcher, FileMatcher } from '@0xkobold/pi-whitelist/matchers'
import { MemorySettingsStore, FileSettingsStore, mergeSettings } from '@0xkobold/pi-whitelist/storage'
import type { PermissionDecision, PermissionRule, PermissionMode } from '@0xkobold/pi-whitelist/types'

Test

npm test        # Run all tests
npm run build   # Compile TypeScript

97 tests across 9 suites covering parser, matchers, storage, merge, errors, readonly, constants, manager, and check.

License

MIT