Skip to content

Commit a2f0328

Browse files
committed
Implement Cross-Platform lrc Auto-Approval
1 parent d97d0ac commit a2f0328

8 files changed

Lines changed: 198 additions & 36 deletions

File tree

plugins/lrc/hooks/hooks.json

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,10 @@
66
"hooks": [
77
{
88
"type": "command",
9-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/plugin-allow-lrc-review-flow.sh",
10-
"args": [],
11-
"timeout": 30,
12-
"statusMessage": "Evaluating scoped auto-approval for lrc review flow"
13-
},
14-
{
15-
"type": "command",
16-
"if": "Bash(git commit *)",
17-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/plugin-blocking-review-git-commit.sh",
9+
"command": "lrc internal claude pre-tool-use",
1810
"args": [],
1911
"timeout": 1260,
20-
"statusMessage": "Running blocking LiveReview gate before git commit"
12+
"statusMessage": "Running LiveReview gate check"
2113
}
2214
]
2315
},
@@ -26,14 +18,13 @@
2618
"hooks": [
2719
{
2820
"type": "command",
29-
"if": "PowerShell(git commit *)",
30-
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/plugin-blocking-review-git-commit.ps1",
21+
"command": "lrc internal claude pre-tool-use",
3122
"args": [],
3223
"timeout": 1260,
33-
"statusMessage": "Running blocking LiveReview gate before git commit"
24+
"statusMessage": "Running LiveReview gate check"
3425
}
3526
]
3627
}
3728
]
3829
}
39-
}
30+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Cross-platform auto-approval hook for the lrc review flow.
4+
5+
Reads a Claude Code PreToolUse JSON payload from stdin and emits an allow
6+
decision when the command matches a known-safe lrc plugin operation.
7+
Exits with no output if no approval rule matches (normal permission flow
8+
continues).
9+
10+
Used by both the Bash-matcher and PowerShell-matcher hooks in hooks.json so
11+
that the same approval rules apply regardless of which shell tool Claude
12+
chose to run the command with.
13+
"""
14+
import json
15+
import os
16+
import shlex
17+
import sys
18+
from pathlib import Path
19+
20+
21+
def emit_allow(reason: str) -> None:
22+
payload = json.dumps(
23+
{
24+
"hookSpecificOutput": {
25+
"hookEventName": "PreToolUse",
26+
"permissionDecision": "allow",
27+
"permissionDecisionReason": reason,
28+
}
29+
}
30+
)
31+
print(payload)
32+
33+
34+
def _normalize_source_prefix(tokens: list) -> list:
35+
"""Strip `source ~/.lrc/env [2>/dev/null || true] &&` prepended by some model runs."""
36+
if len(tokens) >= 4 and tokens[0] == "source" and tokens[1] == "~/.lrc/env" and tokens[2] == "&&":
37+
return tokens[3:]
38+
if (
39+
len(tokens) >= 7
40+
and tokens[0] == "source"
41+
and tokens[1] == "~/.lrc/env"
42+
and tokens[2] == "2>/dev/null"
43+
and tokens[3] == "||"
44+
and tokens[4] == "true"
45+
and tokens[5] == "&&"
46+
):
47+
return tokens[6:]
48+
return tokens
49+
50+
51+
def _paths_equal(a: Path, b: Path) -> bool:
52+
"""Compare two paths, resolving if both exist; otherwise compare normalized strings."""
53+
try:
54+
return a.resolve() == b.resolve()
55+
except Exception:
56+
pass
57+
# Fallback: compare as strings after normalizing separators (handles mixed / and \)
58+
return str(a).replace("\\", "/").lower() == str(b).replace("\\", "/").lower()
59+
60+
61+
def main() -> None:
62+
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "").strip()
63+
plugin_scripts = Path(plugin_root, "scripts") if plugin_root else None
64+
65+
try:
66+
payload = json.load(sys.stdin)
67+
except Exception:
68+
return
69+
70+
command = (payload.get("tool_input", {}).get("command") or "").strip()
71+
if not command:
72+
return
73+
74+
# Parse the command into tokens using POSIX mode (handles both bash and
75+
# PowerShell invocations reasonably well for our whitelist patterns).
76+
try:
77+
tokens = shlex.split(command, posix=True)
78+
except ValueError:
79+
return
80+
81+
tokens = _normalize_source_prefix(tokens)
82+
if not tokens:
83+
return
84+
85+
# ── Rule: bash .../ensure-lrc.sh --for-review ────────────────────────────
86+
# Emitted by review/SKILL.md and hooks/SKILL.md on Unix.
87+
if tokens[0] == "bash" and len(tokens) >= 3 and tokens[-1] == "--for-review":
88+
if plugin_scripts:
89+
try:
90+
if _paths_equal(Path(tokens[1]), plugin_scripts / "ensure-lrc.sh"):
91+
emit_allow("Auto-allow plugin ensure-lrc review readiness command")
92+
return
93+
except Exception:
94+
pass
95+
96+
# ── Rule: PowerShell & .../ensure-lrc.ps1 [-ForReview] ───────────────────
97+
# Emitted by review/SKILL.md and hooks/SKILL.md on Windows via PowerShell tool.
98+
# Handles both:
99+
# & "C:\...\ensure-lrc.ps1" -ForReview
100+
# & "C:\...\ensure-lrc.ps1"
101+
# "C:\...\ensure-lrc.ps1" -ForReview (direct path invocation)
102+
if plugin_scripts:
103+
path_token_idx = None
104+
if tokens[0] == "&" and len(tokens) >= 2:
105+
path_token_idx = 1
106+
elif len(tokens) >= 1:
107+
# Direct invocation without & operator
108+
first = tokens[0].strip("\"'")
109+
if first.lower().endswith("ensure-lrc.ps1"):
110+
path_token_idx = 0
111+
112+
if path_token_idx is not None:
113+
raw_path = tokens[path_token_idx].strip("\"'")
114+
try:
115+
if _paths_equal(Path(raw_path), plugin_scripts / "ensure-lrc.ps1"):
116+
emit_allow("Auto-allow plugin ensure-lrc PowerShell command")
117+
return
118+
except Exception:
119+
pass
120+
121+
# ── Rule: python3 .../setup-session.py start ─────────────────────────────
122+
if tokens[0] == "python3" and len(tokens) >= 3:
123+
if plugin_scripts:
124+
try:
125+
if _paths_equal(Path(tokens[1]), plugin_scripts / "setup-session.py"):
126+
if tokens[2] == "start" and len(tokens) == 3:
127+
emit_allow("Auto-allow plugin setup-session start command")
128+
return
129+
if tokens[2] == "submit-key" and len(tokens) >= 5 and tokens[3] == "--key":
130+
emit_allow("Auto-allow plugin setup-session submit-key command")
131+
return
132+
except Exception:
133+
pass
134+
135+
# ── Rule: lrc review --staged [--blocking-review] ────────────────────────
136+
if tokens[0] == "lrc" and len(tokens) >= 3 and tokens[1] == "review":
137+
if tokens[2] == "--staged":
138+
if len(tokens) == 3:
139+
emit_allow("Auto-allow lrc review staged command")
140+
return
141+
if len(tokens) == 4 and tokens[3] == "--blocking-review":
142+
emit_allow("Auto-allow lrc review blocking command")
143+
return
144+
145+
146+
if __name__ == "__main__":
147+
main()

plugins/lrc/scripts/setup-session.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,12 @@ def active_worker_pid() -> Optional[int]:
9494

9595
def command_env() -> dict:
9696
env = os.environ.copy()
97-
home_local_bin = str(Path.home() / ".local" / "bin")
98-
env["PATH"] = home_local_bin + os.pathsep + env.get("PATH", "")
97+
paths_to_prepend = [str(Path.home() / ".local" / "bin")]
98+
if os.name == "nt":
99+
local_app_data = os.environ.get("LOCALAPPDATA", "")
100+
if local_app_data:
101+
paths_to_prepend.append(str(Path(local_app_data) / "Programs" / "lrc"))
102+
env["PATH"] = os.pathsep.join(paths_to_prepend) + os.pathsep + env.get("PATH", "")
99103
return env
100104

101105

@@ -112,6 +116,7 @@ def run_worker() -> int:
112116
stdout=subprocess.PIPE,
113117
stderr=subprocess.STDOUT,
114118
text=True,
119+
encoding="utf-8",
115120
bufsize=0,
116121
env=command_env(),
117122
)

plugins/lrc/skills/hooks/SKILL.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
description: Inspect or toggle LiveReview hook state in the current repository using canonical lrc hook commands.
33
disable-model-invocation: true
44
argument-hint: "status | enable | disable [claude|all]"
5-
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc hooks *)
5+
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc hooks *) PowerShell(*ensure-lrc.ps1*) PowerShell(lrc hooks *)
66
---
77

88
# lrc hooks
99

10-
Always ensure the backend is present first:
10+
Always ensure the backend is present first.
1111

12+
On **Windows** use the PowerShell tool:
13+
```powershell
14+
& "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.ps1"
15+
```
16+
17+
On **Unix/macOS** use the Bash tool:
1218
```bash
1319
bash "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.sh"
1420
```

plugins/lrc/skills/review/SKILL.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: Review staged changes in the current repository with the canonical lrc backend.
33
disable-model-invocation: true
44
argument-hint: "[--blocking]"
5-
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(python3 *setup-session.py*) Bash(lrc review *)
5+
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc internal claude setup *) Bash(lrc review *) PowerShell(*ensure-lrc.ps1*) PowerShell(lrc internal claude setup *) PowerShell(lrc review *)
66
---
77

88
# lrc review
@@ -11,16 +11,22 @@ Execution policy (strict):
1111
1. Execute commands immediately. Do not ask generic follow-up questions.
1212
2. Never respond with "How can I help" or similar conversational fallback.
1313
3. If no extra user qualifier is provided, default to reviewing staged changes.
14-
4. Use Bash tool calls for command execution before any explanatory prose.
14+
4. Use tool calls for command execution before any explanatory prose.
1515

1616
Messaging calibration (strict):
1717
1. Keep status text neutral and concise.
1818
2. Do not frame expected flow as alarming, unexpected, or suspicious.
1919
3. If `lrc review --staged` commits or pushes, treat that as normal successful behavior.
2020
4. Do not use phrases like "unexpected" or "still failing" unless the flow is truly blocked.
2121

22-
Always ensure the backend is present and review-ready before running review commands:
22+
Always ensure the backend is present and review-ready before running review commands.
2323

24+
On **Windows** use the PowerShell tool:
25+
```powershell
26+
& "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.ps1" -ForReview
27+
```
28+
29+
On **Unix/macOS** use the Bash tool:
2430
```bash
2531
bash "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.sh" --for-review
2632
```
@@ -29,31 +35,27 @@ If the command above fails because setup is incomplete:
2935
1. Start setup immediately. Do not ask for an extra yes/no confirmation.
3036

3137
```bash
32-
python3 "${CLAUDE_SKILL_DIR}/../../scripts/setup-session.py" start
38+
lrc internal claude setup start
3339
```
3440

3541
2. Tell the user: setup has two steps in order: first complete Hexmos account login in the browser flow, then get a Gemini API key from Google AI Studio; if the browser does not open automatically, complete Hexmos login as needed and open https://aistudio.google.com/ manually; after both steps, paste only the Gemini API key in the next chat reply.
3642
3. When the user replies with the Gemini API key, submit it immediately:
3743

3844
```bash
39-
python3 "${CLAUDE_SKILL_DIR}/../../scripts/setup-session.py" submit-key --key "$USER_MESSAGE"
45+
lrc internal claude setup submit-key --key "$USER_MESSAGE"
4046
```
4147

42-
4. After key submission succeeds, run exactly these commands in order and then stop:
43-
44-
```bash
45-
bash "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.sh" --for-review
46-
```
48+
4. After key submission succeeds, run the ensure command again (platform-appropriate, as above) and then stop.
4749

4850
Then run the mapped review command for this request (`lrc review --staged`, `lrc review --staged --blocking-review`, or `lrc review --commit HEAD`).
4951

50-
If the second `ensure-lrc.sh --for-review` still reports setup required immediately after a successful `submit-key`, do not run exploratory diagnostics. Proceed directly to the mapped `lrc review` command once.
52+
If the second ensure step still reports setup required immediately after a successful `submit-key`, do not run exploratory diagnostics. Proceed directly to the mapped `lrc review` command once.
5153

5254
Do not run exploratory diagnostics (`lrc status`, `ls`, `cat`, etc.) unless one of the two commands above fails.
5355

5456
Do not prefix canonical commands with `source ~/.lrc/env &&`.
5557

56-
Do not inspect `~/.lrc.toml` with Read/file tools to decide readiness. Use only the canonical `ensure-lrc.sh --for-review` and `lrc setup` flow.
58+
Do not inspect `~/.lrc.toml` with Read/file tools to decide readiness. Use only the canonical ensure script and `lrc setup` flow.
5759

5860
Then map the user request to the canonical command:
5961

plugins/lrc/skills/skip/SKILL.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
---
22
description: Skip AI review for the staged tree and write an attestation with the canonical lrc backend.
33
disable-model-invocation: true
4-
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc review *)
4+
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc review *) PowerShell(*ensure-lrc.ps1*) PowerShell(lrc review *)
55
---
66

77
# lrc skip
88

9-
Always ensure the backend is present first:
9+
Always ensure the backend is present first.
1010

11+
On **Windows** use the PowerShell tool:
12+
```powershell
13+
& "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.ps1"
14+
```
15+
16+
On **Unix/macOS** use the Bash tool:
1117
```bash
1218
bash "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.sh"
1319
```

plugins/lrc/skills/ui/SKILL.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
---
22
description: Open the canonical git-lrc UI flow for managing setup and connectors.
33
disable-model-invocation: true
4-
allowed-tools: Bash(source ~/.lrc/env*) Bash(lrc ui*)
4+
allowed-tools: Bash(lrc ui*) PowerShell(lrc ui*)
55
---
66

77
# lrc ui
88

99
Run the canonical UI command:
1010

1111
```bash
12-
source ~/.lrc/env 2>/dev/null || true
1312
lrc ui
1413
```
1514

plugins/lrc/skills/vouch/SKILL.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
---
22
description: Manually vouch for the staged tree without AI review using the canonical lrc backend.
33
disable-model-invocation: true
4-
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc review *)
4+
allowed-tools: Bash(bash *ensure-lrc.sh*) Bash(lrc review *) PowerShell(*ensure-lrc.ps1*) PowerShell(lrc review *)
55
---
66

77
# lrc vouch
88

9-
Always ensure the backend is present first:
9+
Always ensure the backend is present first.
1010

11+
On **Windows** use the PowerShell tool:
12+
```powershell
13+
& "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.ps1"
14+
```
15+
16+
On **Unix/macOS** use the Bash tool:
1117
```bash
1218
bash "${CLAUDE_SKILL_DIR}/../../scripts/ensure-lrc.sh"
1319
```

0 commit comments

Comments
 (0)