{"id":48842532,"url":"https://github.com/nnemirovsky/claude-passthru","last_synced_at":"2026-04-15T03:01:46.272Z","repository":{"id":351345795,"uuid":"1210592291","full_name":"nnemirovsky/claude-passthru","owner":"nnemirovsky","description":"Regex-based permission rules for Claude Code via hooks","archived":false,"fork":false,"pushed_at":"2026-04-14T16:33:33.000Z","size":188,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-14T17:24:56.977Z","etag":null,"topics":["claude-code","claude-code-hook","claude-code-plugin","developer-tools","permissions","regex","security"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nnemirovsky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-14T15:06:55.000Z","updated_at":"2026-04-14T16:33:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nnemirovsky/claude-passthru","commit_stats":null,"previous_names":["nnemirovsky/claude-passthru"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/nnemirovsky/claude-passthru","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnemirovsky%2Fclaude-passthru","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnemirovsky%2Fclaude-passthru/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnemirovsky%2Fclaude-passthru/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnemirovsky%2Fclaude-passthru/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nnemirovsky","download_url":"https://codeload.github.com/nnemirovsky/claude-passthru/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnemirovsky%2Fclaude-passthru/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31824118,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T18:05:02.291Z","status":"online","status_checked_at":"2026-04-15T02:00:06.175Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude-code","claude-code-hook","claude-code-plugin","developer-tools","permissions","regex","security"],"created_at":"2026-04-15T03:01:45.264Z","updated_at":"2026-04-15T03:01:46.266Z","avatar_url":"https://github.com/nnemirovsky.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# passthru\n\nRegex-based permission rules for Claude Code via hooks.\n\n[![Tests](https://github.com/nnemirovsky/claude-passthru/actions/workflows/tests.yml/badge.svg)](https://github.com/nnemirovsky/claude-passthru/actions/workflows/tests.yml)\n[![Release](https://img.shields.io/github/v/release/nnemirovsky/claude-passthru)](https://github.com/nnemirovsky/claude-passthru/releases)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\nThe native permission system in Claude Code only takes glob wildcards at the end of a pattern, which leaves big gaps. `passthru` adds a thin regex layer in front of it so you can auto-allow (or deny) tool calls by shape instead of listing each command. It sits on top of your existing `settings.json` and leaves everything that does not match to the native dialog.\n\n## Quick example\n\nNative `Bash(bash /Users/you/project/:*)` does not match `bash /Users/you/project/script.sh` because Claude Code enforces a word boundary after the prefix. You end up listing every script by name, or giving up and granting the full `Bash(bash:*)` namespace.\n\nWith passthru, one rule does what you meant in the first place:\n\n```json\n{ \"tool\": \"Bash\", \"match\": { \"command\": \"^bash /Users/you/project/\" }, \"reason\": \"run project scripts\" }\n```\n\nMore examples: shape-matching a `gh api` endpoint across any owner/repo pair, allowing every tool on an MCP server, denying `rm -rf /` globally. See [Rule format reference](#rule-format-reference) and [`docs/examples.md`](docs/examples.md).\n\n## Install\n\n```\n/plugin marketplace add nnemirovsky/claude-passthru\n/plugin install passthru\n```\n\n## What you can do\n\n* **Regex-based Bash prefixes.** Auto-allow a directory of scripts, a shell pipeline, or any command family the native glob syntax cannot express.\n* **Shape-aware path and URL rules.** Match on the structure of a path or URL (e.g. `^gh api /repos/[^/]+/[^/]+/forks`) so you pin the endpoint, not the owner.\n* **MCP tool namespaces.** Allow a whole MCP server family with a single tool-regex rule, no need to enumerate every tool.\n* **Deny lists that win.** A matching deny rule unconditionally overrides any allow, so you can cement safety rules on top of a permissive allow set.\n* **Opt-in audit log.** JSONL record of every decision (including what the native dialog did for passthroughs). Off by default, zero overhead when disabled.\n* **Standalone verifier.** Validate every rule file from the command line or via `/passthru:verify` to catch bad JSON, invalid regex, and allow/deny conflicts before they silently disable rules.\n* **First-run bootstrap.** One-shot `/passthru:bootstrap` command (or `scripts/bootstrap.sh` for scripting) that converts existing native `permissions.allow` entries into passthru rules. A one-time `SessionStart` hint points at it when there are importable entries.\n\n## Commands\n\nAll commands are plugin-namespaced under `/passthru:`.\n\n| Command | What it does |\n| --- | --- |\n| `/passthru:bootstrap` | One-shot importer: reviews your existing `permissions.allow` entries, shows the proposed rules, asks to confirm, then writes `passthru.imported.json`. Runs the verifier afterwards. |\n| `/passthru:add` | Add a rule without hand-editing `passthru.json`. Supports `--deny` and `--field`. |\n| `/passthru:suggest` | Propose a generalized rule from a recent tool call in the conversation, then write it on confirmation. |\n| `/passthru:verify` | Validate every rule file. Surfaces parse errors, schema violations, invalid regex, duplicates, and allow/deny conflicts. |\n| `/passthru:log` | Read the audit log with filters. Also toggles the audit sentinel on/off. |\n\nFull reference in the [Command reference](#command-reference) section below.\n\n---\n\n## Requirements\n\nRuntime dependencies the plugin needs on the user's machine.\n\n* **bash 3.2+** or **bash 4.0+**. The hook scripts are written to POSIX/bash 3.2 (no associative arrays, no `declare -n`, no `mapfile`). macOS ships bash 3.2 by default and Linux distros ship bash 4+, both work.\n* **jq 1.6+**. Used to parse rule files and build JSON output.\n  * macOS: `brew install jq`\n  * Debian/Ubuntu: `apt install jq`\n  * RHEL/Fedora: `dnf install jq`\n* **perl 5+**. Used as the PCRE regex engine because BSD grep on macOS lacks `-P`. Preinstalled on macOS and essentially every Linux distribution.\n* **bats-core 1.9+** (tests only, not required to run the plugin).\n  * macOS: `brew install bats-core`\n  * Debian/Ubuntu: `apt install bats` (usually older, prefer npm)\n  * npm (any platform): `npm install -g bats`\n\n**PowerShell support:** the hook itself is Bash plus perl only. PowerShell rule matching works because Claude Code still invokes the `PreToolUse` hook for `PowerShell` tool calls. No PowerShell runtime is needed on the user's machine for the plugin itself.\n\n## How it works\n\nNative rules solve the common case. They fall short when:\n\n* The thing you want to match is not space-delimited after a prefix (directory paths, URL paths).\n* You need to pin the shape of a sub-argument, not just the leading verb.\n* You want to allow a whole MCP server family without listing every tool.\n* You want a deny list that unconditionally overrides a more permissive allow.\n\n`passthru` adds a thin regex layer in front of the native system. When a passthru rule matches, the hook emits a decision and Claude Code skips the permission dialog. When nothing matches, control passes through to the native rules unchanged. Nothing about your existing `settings.json` or `.claude/settings.local.json` changes.\n\nWorks across every tool Claude Code exposes (`Bash`, `PowerShell`, `Read`, `Edit`, `Write`, `WebFetch`, MCP tools, and so on).\n\n## First-run bootstrap\n\nThe plugin ships a bootstrap importer that converts existing native `permissions.allow` entries into passthru rule files. It reads up to three settings files: the user-scope `~/.claude/settings.json`, the project-scope shared `./.claude/settings.json`, and the project-scope local `./.claude/settings.local.json`. Run it once after install to avoid starting from zero.\n\n**Recommended:** run `/passthru:bootstrap` inside a Claude Code session. It dry-runs first, shows the rules it would import, asks you to confirm, then writes and verifies. Use `--user-only` or `--project-only` to narrow the scope.\n\n**Non-interactive:** the same logic is available as a plain shell script for CI or ad-hoc use. Dry run first (prints proposed rules to stdout, writes nothing):\n\n```\nbash ~/.claude/plugins/marketplaces/nnemirovsky/claude-passthru/scripts/bootstrap.sh\n```\n\nThe exact path depends on where Claude Code installed the plugin. If you cloned the repo directly, the script lives at `scripts/bootstrap.sh` in your clone. Inspect the output, then re-run with `--write` to persist:\n\n```\nbash .../scripts/bootstrap.sh --write\n```\n\n`--write` mode also runs `scripts/verify.sh --quiet` after writing. If the verifier finds errors, the script restores the pre-write backup and exits non-zero.\n\n**What bootstrap converts.** Six native rule shapes are recognized:\n\n| Native rule | Converted to |\n| --- | --- |\n| `Bash(\u003cprefix\u003e:*)` | `{\"tool\": \"Bash\", \"match\": {\"command\": \"^\u003cprefix\u003e(\\\\s|$)\"}}` |\n| `Bash(\u003cexact command\u003e)` | `{\"tool\": \"Bash\", \"match\": {\"command\": \"^\u003cexact\u003e$\"}}` |\n| `mcp__server__tool` | `{\"tool\": \"^mcp__server__tool$\"}` |\n| `WebFetch(domain:x.com)` | `{\"tool\": \"WebFetch\", \"match\": {\"url\": \"^https?://([^/.]+\\\\.)*x\\\\.com([/:?#]\\|$)\"}}` |\n| `WebSearch` | `{\"tool\": \"^WebSearch$\"}` |\n| `Read(\u003cpath\u003e)`, `Edit(\u003cpath\u003e)`, `Write(\u003cpath\u003e)` | `{\"tool\": \"^Read$\", \"match\": {\"file_path\": \"^\u003cpath\u003e$\"}}` (exact) or `\"^\u003cpath\u003e(/\\|$)\"` when the native rule ends in `/**` or `/*` |\n| `Skill(\u003cname\u003e)` | `{\"tool\": \"^Skill$\", \"match\": {\"skill\": \"^\u003cname\u003e$\"}}` |\n\nRegex metacharacters in the original path/prefix/name are escaped so the converted pattern matches literally. Anything that does not match one of the shapes above is skipped with a `[WARN]` line on stderr (for example, custom MCP tool patterns that do not start with `mcp__`, or a `WebFetch(...)` with a non-`domain:` argument).\n\nFor `Read`, `Edit`, and `Write`, path acceptance mirrors Claude Code's own rules (`src/utils/permissions/pathValidation.ts`): redundant slash runs (`//foo`, `///foo/bar`) are collapsed to a single slash, `~/...` expands to `$HOME/...`, and paths with spaces or deep nesting are accepted. Only the shapes Claude Code itself rejects are skipped with a `[WARN]`:\n\n* shell / env expansion: `$VAR`, `${VAR}`, `$(cmd)`, `%VAR%`\n* zsh equals expansion: leading `=` (e.g. `=cmd`)\n* tilde variants other than `~/`: `~user`, `~+`, `~-`, `~N`\n* UNC paths: leading `\\\\server\\share`\n\nBootstrap writes to dedicated imported files so hand-curated rules in `passthru.json` stay separate:\n\n* `~/.claude/passthru.imported.json` (user scope)\n* `.claude/passthru.imported.json` (project scope)\n\nRe-running bootstrap overwrites the imported files. Edit `passthru.json` (the authored file) for hand-managed rules. Both files are merged at hook time.\n\n**One-time session hint.** The plugin also ships a `SessionStart` hook that detects when you have importable `permissions.allow` entries but no passthru rule files yet. On the first such session it prints a single-line hint to stderr pointing at `/passthru:bootstrap`, then records a marker at `~/.claude/passthru.bootstrap-hint-shown` so the hint never fires again. Delete that marker file to re-enable the hint.\n\n## Rule format reference\n\nRule files are JSON with the shape:\n\n```json\n{\n  \"version\": 1,\n  \"allow\": [ { \"tool\": \"...\", \"match\": { \"...\": \"...\" }, \"reason\": \"...\" } ],\n  \"deny\":  [ { \"tool\": \"...\", \"match\": { \"...\": \"...\" }, \"reason\": \"...\" } ]\n}\n```\n\nFour examples covering common use cases.\n\n**Directory prefix (Bash).** Auto-allow any `bash` invocation against a scripts dir:\n\n```json\n{ \"tool\": \"Bash\", \"match\": { \"command\": \"^bash /Users/you/scripts/\" }, \"reason\": \"local scripts\" }\n```\n\n**Regex on gh api endpoints (Bash).** Auto-allow repo forks queries across any owner/repo:\n\n```json\n{ \"tool\": \"Bash\", \"match\": { \"command\": \"^gh api /repos/[^/]+/[^/]+/forks\" }, \"reason\": \"github forks api reads\" }\n```\n\n**MCP namespace (no match block).** Auto-allow every tool on the `gemini-cli` MCP server:\n\n```json\n{ \"tool\": \"^mcp__gemini-cli__\", \"reason\": \"gemini mcp server\" }\n```\n\n**Deny rule (priority over allow).** Block destructive `rm -rf /` patterns across any shell tool, even if a broader allow would match:\n\n```json\n{ \"tool\": \"Bash|PowerShell\", \"match\": { \"command\": \"rm\\\\s+-rf\\\\s+/\" }, \"reason\": \"safety\" }\n```\n\nSee [`docs/rule-format.md`](docs/rule-format.md) for the full schema reference and [`docs/examples.md`](docs/examples.md) for more examples.\n\n## Command reference\n\nAll commands are plugin-namespaced under `/passthru:`.\n\n### `/passthru:add`\n\nAdd a rule without hand-editing `passthru.json`. Canonical call:\n\n```\n/passthru:add user Bash \"^gh api /repos/[^/]+/[^/]+/forks\" \"github forks api reads\"\n```\n\nFlags: `--deny` (write to deny list instead of allow), `--field \u003cname\u003e` (override the default `tool_input` field).\n\n### `/passthru:suggest`\n\nPropose a generalized rule from a recent tool call in the conversation. The command scans the transcript, drafts a regex that generalizes owner / repo / version-style variables, shows matched and non-matched examples, and on confirmation hands off to the same write wrapper `/passthru:add` uses.\n\n```\n/passthru:suggest gh api\n```\n\n### `/passthru:verify`\n\nValidate every rule file. Surfaces parse errors, schema violations, invalid regex, duplicates, and allow+deny conflicts.\n\n```\n/passthru:verify\n/passthru:verify --scope user --strict\n```\n\n### `/passthru:log`\n\nRead the audit log in a filtered table (see [Audit log](#audit-log) below). Also toggles the audit sentinel.\n\n```\n/passthru:log --since 1h --tail 20\n/passthru:log --enable\n```\n\n## Verifier standalone\n\nThe verifier can be run without Claude Code attached:\n\n```\nbash scripts/verify.sh [--scope user|project|all] [--strict] [--format plain|json] [--quiet]\n```\n\nExit codes:\n\n* `0` - clean (no errors, no warnings, or warnings without `--strict`).\n* `1` - one or more errors (bad JSON, schema violation, invalid regex, allow+deny conflict).\n* `2` - warnings only (duplicates, shadowing) and `--strict` is set.\n\n## Verifying rules\n\nRun `/passthru:verify` (or `bash scripts/verify.sh`) whenever you edit a `passthru.json` file by hand. The hook silently skips malformed rule files at runtime so a typo can quietly disable your rules. The verifier surfaces the failure up front.\n\nAutomatic verification already covers every machine-driven write path. The following all call `scripts/write-rule.sh`, which takes a backup, writes the rule, runs the verifier, and restores the backup if verification fails:\n\n* `/passthru:add` slash command\n* `/passthru:suggest` slash command\n* `scripts/bootstrap.sh --write`\n\nSo the only time you need to run the verifier manually is after editing `passthru.json` with an editor.\n\nInterpret the output as follows:\n\n* `[OK] N rules across M files checked` - nothing to do.\n* `[ERR] \u003cfile\u003e:\u003cjq-path\u003e [rule N] \u003cmsg\u003e` - fix the listed file and re-run.\n* `[WARN] ...` - duplicates or shadowing. Harmless by default. Add `--strict` to treat as errors.\n\n## Test locally\n\nTo iterate on the plugin without installing it through the marketplace, load it straight from a working directory:\n\n```\nclaude --plugin-dir /path/to/claude-passthru\n```\n\nThis is the fastest dev loop. Every time you restart Claude Code the plugin is re-read from disk. No `/plugin install`, no cache flush, no uninstall step between iterations.\n\n**Heads-up:** the plugin self-allow regex matches the canonical marketplace install path (`~/.claude/plugins/.../claude-passthru/scripts/\u003cname\u003e.sh`). When you load the plugin via `--plugin-dir` from a clone elsewhere on disk, that regex does not match, and slash commands like `/passthru:add` will hit the native permission dialog the first time. Either accept the dialog once per shell, or add a temporary one-line allow rule to your own `passthru.json` matching the dev path. The self-allow is intentionally narrow to prevent rogue scripts from impersonating the plugin.\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full dev workflow including running tests and pipe-testing the hook.\n\n## Audit log\n\nThe plugin can record every permission decision to a JSONL file at `~/.claude/passthru-audit.log`. Audit is **opt-in and off by default**. When disabled, the hook does a single `-e` check on the sentinel file and moves on, so there is effectively zero overhead.\n\n**Enable:**\n\n```\ntouch ~/.claude/passthru.audit.enabled\n```\n\nor\n\n```\n/passthru:log --enable\n```\n\n**Disable:**\n\n```\nrm ~/.claude/passthru.audit.enabled\n```\n\nor\n\n```\n/passthru:log --disable\n```\n\n**Log path:** `~/.claude/passthru-audit.log` (JSONL, one event per line).\n\n**Event types.** From the `PreToolUse` hook:\n\n* `allow` - a passthru allow rule matched.\n* `deny` - a passthru deny rule matched.\n* `passthrough` - no passthru rule matched. Control passed to the native permission system.\n\nFrom the `PostToolUse` hook, classifying what the native dialog decided for a passthrough:\n\n* `asked_allowed_once` - user picked \"allow once\" in the native dialog.\n* `asked_allowed_always` - user picked \"allow always\" (native `settings.json` got a new entry).\n* `asked_denied_once` - user denied once.\n* `asked_denied_always` - user denied permanently.\n* `asked_allowed_unknown` - outcome could not be classified (e.g. session ended mid-dialog).\n\n**View the log:**\n\n```\n/passthru:log\n/passthru:log --since 1h --event '^asked_'\nbash scripts/log.sh --format raw | jq .\n```\n\n**Rotation.** None built in. The audit file grows one line per tool call when enabled. Use `logrotate`, `cron`-driven `truncate`, or manually rotate when it gets large.\n\n## Troubleshooting\n\n* **Disable every rule without uninstalling.** `touch ~/.claude/passthru.disabled` turns the plugin into a no-op (the hook sees the sentinel and returns passthrough immediately). Remove the file to re-enable.\n* **Bad rules after a manual edit.** Run `/passthru:verify` or `bash scripts/verify.sh` to see exactly which file, path, and message failed.\n* **Rules are not firing.** Launch Claude Code with `claude --debug` and watch the hook output. The handler prints its decision reason to stderr, which `--debug` surfaces.\n* **Concurrent writes or a stuck lock.** `scripts/write-rule.sh` serializes writers under a single user-scope lock at the directory `~/.claude/passthru.write.lock.d`. The lock uses `mkdir`, which is atomic on every POSIX filesystem, so no `flock(1)` is required. If the process that held the lock died without releasing it, remove the directory manually (`rmdir ~/.claude/passthru.write.lock.d`). Lock-acquisition timeout defaults to 5 seconds and can be overridden via `PASSTHRU_WRITE_LOCK_TIMEOUT=\u003cseconds\u003e` in the environment.\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for the dev loop, test commands, and rule schema evolution policy.\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnnemirovsky%2Fclaude-passthru","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnnemirovsky%2Fclaude-passthru","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnnemirovsky%2Fclaude-passthru/lists"}