Skip to content

Commit 8f6ba76

Browse files
authored
feat: /passthru:bootstrap command and SessionStart hint (#3)
1 parent 5ce4716 commit 8f6ba76

10 files changed

Lines changed: 721 additions & 6 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "passthru",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Regex-based permission rules for Claude Code via hooks",
55
"owner": {
66
"name": "nnemirovsky"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "passthru",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Regex-based permission rules for Claude Code via hooks",
55
"license": "MIT"
66
}

CLAUDE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ Developer-facing notes for future Claude sessions working on this repo.
99
plugin.json plugin manifest (name, version, description)
1010
marketplace.json marketplace manifest (used when published)
1111
commands/
12+
bootstrap.md /passthru:bootstrap slash command (wraps scripts/bootstrap.sh with dry-run + confirm)
1213
add.md /passthru:add slash command (prompt-based)
1314
suggest.md /passthru:suggest slash command (prompt-based)
1415
verify.md /passthru:verify slash command (prompt-based)
1516
log.md /passthru:log slash command (prompt-based)
1617
hooks/
17-
hooks.json registers PreToolUse + PostToolUse handlers with matcher "*", timeout 10s each
18+
hooks.json registers PreToolUse + PostToolUse (timeout 10s each, matcher "*") and
19+
SessionStart (timeout 5s, no matcher) handlers
1820
common.sh shared library. Functions:
1921
* load_rules / validate_rules (merge + schema-check)
2022
* pcre_match / match_rule / find_first_match (rule matching)
@@ -27,6 +29,9 @@ hooks/
2729
handlers/
2830
pre-tool-use.sh main hook: loads rules, matches, emits allow/deny/passthrough
2931
post-tool-use.sh classifies native-dialog outcomes into asked_* events (audit only)
32+
session-start.sh one-time bootstrap hint. Gated by ~/.claude/passthru.bootstrap-hint-shown
33+
marker. Prints to stderr only when the user has no passthru files yet
34+
AND has importable entries in ~/.claude/settings.json.
3035
scripts/
3136
bootstrap.sh one-time importer from native permissions.allow into passthru.imported.json
3237
write-rule.sh atomic write wrapper: backup + append + verify + rollback

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@ More examples: shape-matching a `gh api` endpoint across any owner/repo pair, al
3535
* **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.
3636
* **Opt-in audit log.** JSONL record of every decision (including what the native dialog did for passthroughs). Off by default, zero overhead when disabled.
3737
* **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.
38-
* **First-run bootstrap.** One-shot importer that converts existing native `permissions.allow` entries into passthru rules.
38+
* **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.
3939

4040
## Commands
4141

4242
All commands are plugin-namespaced under `/passthru:`.
4343

4444
| Command | What it does |
4545
| --- | --- |
46+
| `/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. |
4647
| `/passthru:add` | Add a rule without hand-editing `passthru.json`. Supports `--deny` and `--field`. |
4748
| `/passthru:suggest` | Propose a generalized rule from a recent tool call in the conversation, then write it on confirmation. |
4849
| `/passthru:verify` | Validate every rule file. Surfaces parse errors, schema violations, invalid regex, duplicates, and allow/deny conflicts. |
@@ -84,9 +85,11 @@ Works across every tool Claude Code exposes (`Bash`, `PowerShell`, `Read`, `Edit
8485

8586
## First-run bootstrap
8687

87-
The plugin ships a bootstrap script that converts existing native `permissions.allow` entries into passthru rule files. The script 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.
88+
The 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.
8889

89-
Dry run first (prints proposed rules to stdout, writes nothing):
90+
**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.
91+
92+
**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):
9093

9194
```
9295
bash ~/.claude/plugins/marketplaces/nnemirovsky/claude-passthru/scripts/bootstrap.sh
@@ -107,6 +110,8 @@ Bootstrap writes to dedicated imported files so hand-curated rules in `passthru.
107110

108111
Re-running bootstrap overwrites the imported files. Edit `passthru.json` (the authored file) for hand-managed rules. Both files are merged at hook time.
109112

113+
**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.
114+
110115
## Rule format reference
111116

112117
Rule files are JSON with the shape:

commands/bootstrap.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
---
2+
description: "Import existing native permission rules into passthru"
3+
argument-hint: "[--user-only|--project-only]"
4+
---
5+
6+
# /passthru:bootstrap
7+
8+
Convert existing native Claude Code `permissions.allow` entries (from
9+
`~/.claude/settings.json` and `./.claude/settings{,.local}.json`) into
10+
passthru rule files, with an interactive preview before anything is
11+
written.
12+
13+
This is the in-session wrapper around `scripts/bootstrap.sh`. The shell
14+
script is always available for non-interactive use, but this command is
15+
the recommended first-run path: it shows exactly what will be imported,
16+
asks for confirmation, and then verifies the result.
17+
18+
Hand-authored `passthru.json` files are never touched. Imports always
19+
land in `passthru.imported.json` (separately per scope). Re-running this
20+
command overwrites the imported files in place.
21+
22+
## What you must do
23+
24+
You are Claude. Drive the workflow below. Surface errors verbatim. Do
25+
not skip the confirmation step.
26+
27+
### 1. Dry-run to collect the proposed rules
28+
29+
Invoke the bootstrap script WITHOUT `--write`, passing `$ARGUMENTS`
30+
through so the user's `--user-only` / `--project-only` choice is
31+
honored:
32+
33+
```bash
34+
bash ${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.sh $ARGUMENTS
35+
```
36+
37+
Capture stdout, stderr, and the exit code. The script emits:
38+
39+
- `# would write: <path>` comment lines (one per scope in play).
40+
- A pretty-printed JSON document per scope, of shape
41+
`{"version":1,"allow":[...],"deny":[]}`.
42+
- `[WARN] skipping ...` lines on stderr for unsupported native rule
43+
forms (spaces past the prefix, unusual `WebFetch(...)` forms, etc.).
44+
These are informational, not fatal.
45+
46+
Non-zero exit means the script itself failed (malformed
47+
`settings.json`, for example). In that case, surface the error
48+
verbatim and stop.
49+
50+
### 2. Count the proposed rules
51+
52+
Parse the dry-run output. For each `# would write:` block, count
53+
`.allow | length` in the JSON document that follows it. Record the
54+
count per scope (`user` and/or `project`) and the grand total.
55+
56+
### 3. Handle the nothing-to-import case
57+
58+
If the grand total is zero (no scopes have any rules, or `$ARGUMENTS`
59+
limited the run to a scope with no importable rules), tell the user:
60+
61+
> Nothing to import. Your `settings.json` has no convertible
62+
> `permissions.allow` entries in the selected scope.
63+
64+
Then suggest one of these next steps, pick whichever fits the
65+
situation:
66+
67+
- `/passthru:add <scope> <tool> <pattern> <reason>` to author a rule
68+
by hand.
69+
- `/passthru:suggest <hint>` to generalize a rule from a recent tool
70+
call in the conversation.
71+
72+
Stop there. Do not invoke `--write`.
73+
74+
### 4. Show the proposal
75+
76+
Otherwise, present the proposal in this order:
77+
78+
1. A one-line summary: `Found N importable rule(s): user=<u>,
79+
project=<p>.` (omit the zero-count scope).
80+
2. For each scope with rules, show the proposed rules. A compact
81+
format is fine - list each rule's `tool` field plus a summary of
82+
the `match` block (or `(namespace rule, no match)` for MCP
83+
namespace rules). Quote the reason if present.
84+
3. Explain where they land:
85+
> This will import N rule(s) from your existing `settings.json`
86+
> files. Your hand-authored `passthru.json` is never touched;
87+
> imports go to `passthru.imported.json` (separately per scope).
88+
> Re-running this command overwrites the imported files in place.
89+
4. If the dry-run emitted any `[WARN]` lines on stderr, show them
90+
and explain that those native entries were skipped because the
91+
converter does not have a safe regex translation for their shape.
92+
The user can still add them via `/passthru:add` or
93+
`/passthru:suggest`.
94+
95+
### 5. Ask for confirmation
96+
97+
Ask the user plainly: "Write these rules now? (yes / no)". Wait for a
98+
clear answer.
99+
100+
- `yes` / `y` / `write` -> proceed to step 6.
101+
- `no` / `n` / `cancel` -> tell the user nothing was written and
102+
stop. Remind them they can re-run `/passthru:bootstrap` when they
103+
are ready, or use `/passthru:add` / `/passthru:suggest` instead.
104+
- Ambiguous answer -> re-ask once. If still ambiguous, treat as no.
105+
106+
The scope choice is already fixed by whatever the user passed in
107+
`$ARGUMENTS` (default = both scopes). Do not ask about scope here.
108+
109+
### 6. Write the rules
110+
111+
On confirmation, invoke the script again with `--write` and the same
112+
`$ARGUMENTS`:
113+
114+
```bash
115+
bash ${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.sh --write $ARGUMENTS
116+
```
117+
118+
The script backs up any existing `passthru.imported.json`, writes the
119+
new document, then runs `scripts/verify.sh --quiet`. If the verifier
120+
fails, the script restores the backup and exits non-zero. Surface
121+
non-zero output verbatim and stop.
122+
123+
On success the script prints `wrote <path>` per scope.
124+
125+
### 7. Run the verifier once more
126+
127+
Independently re-run the verifier to confirm the imported files parse
128+
cleanly alongside any existing hand-authored rules:
129+
130+
```bash
131+
bash ${CLAUDE_PLUGIN_ROOT}/scripts/verify.sh
132+
```
133+
134+
If this exits non-zero, show the errors verbatim. The bootstrap
135+
script has already rolled back on its own verify failure, so an exit
136+
here means something else (e.g. a pre-existing problem in
137+
`passthru.json` that the merged view now surfaces).
138+
139+
### 8. Report success
140+
141+
Print a short confirmation:
142+
143+
> Imported N rule(s). Restart Claude Code to pick the new rules up,
144+
> or wait - the PreToolUse hook re-reads every rule file on every
145+
> tool call, so the imports take effect on the very next tool call in
146+
> this session.
147+
148+
If the dry-run emitted `[WARN]` lines for skipped entries, remind the
149+
user once more that they can port those manually via `/passthru:add`
150+
or `/passthru:suggest`.
151+
152+
## Examples
153+
154+
### Both scopes, importable rules in user scope only
155+
156+
Dry-run output:
157+
158+
```
159+
# would write: /Users/you/.claude/passthru.imported.json
160+
{
161+
"version": 1,
162+
"allow": [
163+
{ "tool": "Bash", "match": { "command": "^ls(\\s|$)" }, "reason": "imported from settings" },
164+
{ "tool": "Bash", "match": { "command": "^gh api(\\s|$)" }, "reason": "imported from settings" }
165+
],
166+
"deny": []
167+
}
168+
# would write: /Users/you/project/.claude/passthru.imported.json
169+
{ "version": 1, "allow": [], "deny": [] }
170+
```
171+
172+
Present:
173+
174+
> Found 2 importable rule(s): user=2.
175+
>
176+
> user scope:
177+
> - `Bash` match `command: ^ls(\s|$)` - imported from settings
178+
> - `Bash` match `command: ^gh api(\s|$)` - imported from settings
179+
>
180+
> This will import 2 rules from your existing settings.json files.
181+
> Your hand-authored passthru.json is never touched; imports land in
182+
> passthru.imported.json.
183+
>
184+
> Write these rules now? (yes / no)
185+
186+
After confirmation and `--write`:
187+
188+
> Imported 2 rule(s). Restart Claude Code (or wait - rules take
189+
> effect on the next tool call since the hook re-reads every
190+
> invocation).
191+
192+
### `--user-only`
193+
194+
```
195+
/passthru:bootstrap --user-only
196+
```
197+
198+
Skips the project scope entirely. Only writes
199+
`~/.claude/passthru.imported.json`. Useful when project-scope
200+
`settings.local.json` has a lot of one-off rules you do not want
201+
ported plugin-side.
202+
203+
### Nothing to import
204+
205+
```
206+
/passthru:bootstrap
207+
```
208+
209+
Dry-run emits two empty documents. Respond:
210+
211+
> Nothing to import. Your settings.json has no convertible
212+
> permissions.allow entries in the selected scope.
213+
>
214+
> Next steps:
215+
> - `/passthru:add user Bash "^gh api " "github api reads"` to add a
216+
> rule by hand.
217+
> - `/passthru:suggest gh api` to generalize a rule from a recent
218+
> tool call.
219+
220+
Do not invoke `--write`.

0 commit comments

Comments
 (0)