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

@synthryn/sypi-permissions

v0.6.0-beta.20260816.6a00fcae

Published

Define approval policy and quarantine untrusted content before tool calls reach host authority. Use it when you need explicit allow, deny, or ask rules for paths and commands. Primary capability: the permissions policy source; it has no separate command o

Readme

sypi-permissions

The approval policy source and its user surface.

The extension registers a decider with the host's tool-call authority. The agent loop consults it after the tool_call chain has run, using the arguments that will execute. The authority owns enforcement, gate classes, retry stability, the subagent ceiling, and the audit record. This extension owns the decision and its prompt.

The three answers. The policy consults the active profile's JSONL ruleset. allow passes. deny rejects with a reason. ask opens an interactive prompt with allow once, deny once, always allow, and always deny choices. Either always choice persists a rule to the ruleset file.

Where there is nobody to ask. A plain headless run (sypi -p) fails closed because it has no confirmation UI. A subagent child escalates a would-reject ask (write, exec, or guard hit) to its spawning session through the reply-file channel and waits for the owner's decision. Pure reads without a guard hit stay cheap. Timeout, malformed replies, and missing channels fail closed to deny.

Bash is read semantically (guard.ts). The command line is tokenized and normalized. &&, ;, and | chains are split. Quoting is removed. sudo/env/xargs/timeout/eval/bash -c "…" wrappers are unwrapped. $(…), <(…), and >(…) substitutions are extracted. Each segment is then checked against the ruleset. An explicit deny such as git checkout * rejects the command even when wrapped or chained. A chain is auto-allowed only when every segment matches an allow rule.

Global catastrophic floor. A single floor runs on every bash command that the ruleset would allow, including a blanket {"action":"allow"}. A guard hit becomes a hard deny (blocked by sypi safety floor: …). The floor covers risky rm -rf, git push --force, git push --delete/--mirror/:ref, git reset --hard, git clean -f, history rewrites, recursive chmod/chown, disk wipes, and curl … | sh.

Rulesets cannot disable the floor. The older enforceOnAllow opt-in and *.guard.json files were removed. In the ask flow, a guard hit escalates the prompt with a danger reason. Headless mode denies with that reason. A <ruleset>.guard.json can tune ask-flow escalation through disabled ids and custom command-glob patterns, but it cannot lower the floor.

The approved command is the command that runs. The decision happens after the tool_call chain, in the agent loop, on the exact argument object handed to execute. A later handler that rewrites the input is judged on that rewritten command, which is also the command that executes.

Command transforms and seatbelt wrappers such as rtk and sandbox still run before approval. The prompt therefore shows their output, such as rtk grep … instead of grep …. The input object remains an ordinary object.

Gate classes. Every rejection declares a class. The authority uses the class to decide what happens next:

  • security - covers a ruleset deny, learned deny, the catastrophic floor, quarantine, and always deny. It never degrades or becomes advisory. The authority replays it for the rest of the session. An always allow decision clears it. deny once and dismissed prompts are not recorded, so an identical call asks again. No call passes without a human approval when this class applies.
  • correctness - covers questions that cannot be answered in context, such as a headless run or a subagent without a reply channel. It always includes a remedy and is not replayed because the remedy usually changes the context.
  • policy - covers conventions rather than danger. It rejects once with a remedy, then records an insistent caller's override. This extension does not issue this class today.

Subagent authority. A subagent must not hold authority its owning session lacks. The spawn publishes an authority ceiling telling the child's own authority what it was granted: the tool allowlist, the parent's resolved posture, and no bypass. The child clamps its posture down to that ceiling (never up), is refused the session bypass outright, and has a call to any tool outside the allowlist rejected by the authority before any decider runs — so no prompt and no policy can widen it. A process marked SYPI_SUBAGENT=1 that arrives with no ceiling, or a malformed one, is treated as granted the least rather than as unconstrained, so a spawn path that forgets to publish fails closed.

Profiles-only model. Four seeded rulesets, one per profile (self-seeded on session_start, never overwriting an edited file). Each answers "what may the agent do to my machine" differently, along the axes that matter — shell access, network egress, git history mutation, and whether unknown operations prompt or refuse:

  • trusted (the default): can run shell commands, edit any file, reach the network, and rewrite git history; unfamiliar operations ask before running. The global catastrophic floor still blocks destructive commands.
  • focused: can run shell and build commands, edit the project, and stage local git changes; refuses network tools, git history rewrites, and writes to home configuration or system locations; unfamiliar operations ask before running.
  • cautious: can read and edit files and run local build and test commands; network access, git history changes, and unfamiliar operations ask before running.
  • locked: can read files, edit the project, and run local build and test commands; refuses web browsing, network tools, git history changes, writes to home configuration or system locations, and unrecognized operations.

Interpreter inline-eval and package-manager install commands remain potentially network-capable operations; the active profile decides whether they ask or refuse rather than treating them as ordinary local work.

Approval ergonomics. ~93% of permission prompts get approved, so per-prompt confirmation is mostly theater; this collapses that friction below the floor without ever weakening it. Three additive layers, all off by default (config.ts):

  • Approval posture: the autonomy dial, one profile key approval.posture (one of suggest | copilot | autopilot) (default suggest, read defensively; a broken/absent profile falls back to the safe default). It only affects the ask branch: suggest fast-paths nothing beyond declared globs; copilot also fast-paths any in-cwd edit/write; autopilot additionally fast-paths shell commands via a fail-closed allowlist (below). It never touches the allow branch, so the catastrophic floor there is untouched by posture.
  • autopilot command allowlist: autopilot's local-command fast-path is an allowlist, not a denylist: it fast-paths a command only when every chain segment's binary is a known-safe read/build/local tool (ls cat head tail wc grep rg find pwd echo which file stat tree cd mkdir cp mv touch node python python3 npm pnpm yarn bun make just cargo go tsc vitest jest sed awk jq sort uniq cut, plus git restricted to read/local verbs status diff log show add commit stash branch checkout (never push/remote). Any unrecognized binary prompts: that's the point — every egress tool (curl wget http https aws gcloud az gh mail sendmail dig host nslookup socat scp ssh …, known or novel) is simply absent, so it can never be silently exfiltrated (a denylist would leak on the first tool it forgot). A $(…)-produced binary (unknown at gate time) and the inline-eval form of an allowlisted interpreter (node -e, python -c, vetoed via the egress classifier) also prompt.
  • autoApprove globs: approval.autoApprove path globs that skip confirmation for edit/write only, and only for a path resolved strictly inside cwd. Containment resolves symlinks: the nearest existing ancestor of the target is realpath'd (both sides) before the check, so an in-cwd symlink escaping the tree (cwd/link -> /etc) is rejected: a .. escape, an absolute out-of-cwd path, a symlink-escape, or a bare cwd all fail (and any fs/realpath error degrades to "not contained" → prompt). Never bash, never egress, never a read.
  • Denial-learns-rule: two identical human denials (deny once) of the same action compile into a learned deny: a standing, targeted deny rule stored per-ruleset under the extension state directory (<ruleset>.learned.json, a .json so it never shows up as a switchable ruleset). It is deny-only by construction (loads filter to action:"deny"), so a learned rule can only make a decision more restrictive, never re-approve. It can be cleared in /settings → Permissions.

Floor-preservation proof. The fast-path sits strictly below every safety gate. Order inside the decider: secret pass-through → quarantine (egress gated when tainted) → learned-deny floor → ruleset decision → on allow, the catastrophic floor; on ask, the approval fast-path. The fast-path is gated on a clean ask-level guard (hits.length === 0, so a flagged command still prompts) and re-runs the identical catastrophic floor the allow branch uses (the full built-in guard set with an empty config), so even a per-ruleset disabled pattern cannot let it wave rm -rf / (or any floor op) through. Egress can never reach autopilot's local-command fast-path: it is a fail-closed allowlist (every egress binary is absent), with an egress veto as a second guard for the inline-eval interpreter overlap. Net: auto-approve can only ever fast-path the safe, ordinarily-approved majority; the catastrophic floor, the quarantine, and learned denies all fire regardless of posture/globs, and the session bypass remains the only total opt-out (host-side, re-typed each session, never persisted).

The subagent/role rulesets (readonly, git, default) are retired; subagents use tool allowlists now. A profile maps to its same-named ruleset via profile.permissions; when unset the trusted ruleset is the fallback. Rulesets are per-profile and swap when the profile changes.

One-time no-clobber migration. On session_start the extension reconciles the legacy layout: it rewrites an untouched old personal/work ruleset to its new seed and removes the retired role rulesets plus any stray work.guard.json, but only when a file is byte-identical to a known old seed. Any file you edited never matches and is left exactly as-is, with a one-line notice. Nothing is ever clobbered.

Secret files are deliberately not blocked here; they are sypi-redact's domain, so permissions passes them through to be masked rather than creating deny/override friction.

Session bypass mirror. The session-wide bypass belongs to the authority, which starts every session with it off and refuses it outright where the subagent ceiling withholds it. This extension only mirrors that state to a sypi.bypass session entry ({bypass}) so the statusline can surface it.

Untrusted-content quarantine. Once a session ingests content nobody vouched for, high-risk egress is gated until the human re-arms (a fresh session_start does); reads, edits and recognized local commands stay untouched. Two things arm it: a web/MCP tool result, and the first read of a file in a project the user has not trusted. Everything else fails closed: an unrecognized binary is treated as egress rather than assumed local. The taint state is mirrored to a sypi.taint session entry ({tainted, source?}).

Project trust remains host-owned. This extension does not register a project_trust handler or auto-trust projects. The host's saved decision, default setting, and interactive prompt decide whether a project is trusted; an approval profile cannot override any of them.

Jurisdiction ends at the model. The decision covers tool calls the model asks for, and three extension-API channels pass it entirely: pi.exec (a direct spawn, never a tool call), pi.registerProvider(name, {baseUrl}) (redirects every later provider request, bearer token included, to any host), and the provider-traffic hooks (before_provider_headers and siblings, which receive the live outgoing headers). Extending the gate to cover them would be theatre — extension code runs in-process at your uid and reaches node:child_process and fetch directly, routing around anything placed on the documented API. What exists instead is the host's audit log, which records every pi.exec call and its exit status, every provider registration with its base URL, and every provider-traffic hook registration, each attributed to the extension that made it; the same log carries every authority decision (kind: "gate") and every change to the authority in force (kind: "posture"). Credential values are never recorded.