Catalog npm
Back to packageRELEASE DETAILS

0.2.0

npm

@0xkobold/pi-whitelist

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

ai-agentpermissionspi-packagetool-gatingwhitelist
Published13 May 2026
Direct dependencies7
Transitive dependenciesNot calculated

README

Current package documentation

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:

#ActionBehavior
1Allow onceLet it run this time, don't persist
2Allow alwaysSave a rule, auto-approve future matches
3DenyBlock 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:

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

Permission Modes

ModeBehavior
defaultPrompt for destructive tools, auto-allow read-only tools
bypassPermissionsAllow everything — dangerous, for CI/testing only
planAsk for everything, even read-only tools
acceptEditsAuto-allow file edits, ask for everything else
dontAskAllow 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):

SourceFilePriority
userSettings~/.pi/settings.json0 (lowest)
projectSettings.pi/settings.json1
localSettings.pi/settings.local.json2
flagSettingsCLI flags3
policySettingsEnterprise/org policy4
cliArg--allowedTools5
command/command in session6
sessionIn-memory only7 (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,
})
MethodDescription
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

MatcherForBehavior
GlobMatcherDefault fallbackUses picomatch for glob matching
CommandMatcherBash, PowerShellSplits on &&, ||, ;, | and checks prefix
FileMatcherFileEdit, FileWriteNormalizes paths to POSIX, uses picomatch

Storage

StoreUse
MemorySettingsStoreTesting, ephemeral sessions
FileSettingsStorePersistent 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

ErrorCodeWhen
PermissionErrorVariousBase error class
RuleParseErrorRULE_PARSE_ERRORMalformed rule string
StorageErrorSTORAGE_ERRORFile read/write failure
MatcherErrorMATCHER_ERRORGlob/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

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Declared dependencies 8

Native requirements
7 direct targetsNot calculated transitive count
Package / targetRequirementRelationship
zod
Environment
{
  "local_name": "zod",
  "alias": false,
  "peer_metadata": null
}
^3.24.0runtime
picomatch
Environment
{
  "local_name": "picomatch",
  "alias": false,
  "peer_metadata": null
}
^4.0.0runtime
vitest
Environment
{
  "local_name": "vitest",
  "alias": false,
  "peer_metadata": null
}
^3.1.0development
typescript
Environment
{
  "local_name": "typescript",
  "alias": false,
  "peer_metadata": null
}
^5.7.0development
@types/node
Environment
{
  "local_name": "@types/node",
  "alias": false,
  "peer_metadata": null
}
^22.0.0development
@types/picomatch
Environment
{
  "local_name": "@types/picomatch",
  "alias": false,
  "peer_metadata": null
}
^4.0.0development
@earendil-works/pi-coding-agent
Environment
{
  "local_name": "@earendil-works/pi-coding-agent",
  "alias": false,
  "peer_metadata": null
}
^0.74.0development
@earendil-works/pi-coding-agent
Environment
{
  "local_name": "@earendil-works/pi-coding-agent",
  "alias": false,
  "peer_metadata": null
}
>=0.74.0peer

Distribution files

1 collected
Distribution 1

Source distribution

Source file
Checksums from source
{
  "shasum": "f339fb379f860cc795fc2a1e4c8afbd7a70d458f",
  "tarball": "https://registry.npmjs.org/@0xkobold/pi-whitelist/-/pi-whitelist-0.2.0.tgz",
  "fileCount": 95,
  "integrity": "sha512-NRwokWuqcMz2dtfsYltRPXzbpVLaSfOFZtj6i2NyrvxGKmE5PSqwYHOiA1XUUrW7v71+/KtXJVMDw1j+zryg6Q==",
  "signatures": [
    {
      "sig": "MEQCIDJpfxXlVnhoNtZqcw1uXDMuhd36e79FkEaEwBB6X7hXAiATRSeFvChuiEIPjBunTWhB1jg+B8Iv9vxgVHcyCoES1w==",
      "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
    }
  ],
  "unpackedSize": 116443
}

Repository

ACCESSIBLE
0xkobold/pi-whitelist

Shared repository data · GitHub

License detected in repository
Not collected
Last push
14 May 2026
Archived
No
Contributor identities
— · partial collection

Public sponsor counts have not been collected. These signals belong to the repository owner.

Downloads & popularity

0count
Gauge · observed snapshots22 Oct 2026
10.50
2026-10-22: 0 · observed 2026-10-22T16:57:25.8520907Z
22 Oct22 Oct

github · 1 observations shown. Missing days are unknown.

View observed values
Date / source periodValueObserved at
2026-10-2202026-10-22T16:57:25.8520907Z

Metadata history

Meaningful changes
Current collected state Current

Observed since 21 Oct, 09:03 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package_version:02b68f210511bbde8b279235ca705dcf24d648cd15a50e3556d1992f89db7ba0

Other collected versions

0.5.214 May0.5.113 May0.5.013 May0.4.213 May0.4.113 May0.4.013 MayView all collected versions →