import from local vendored copy (2026-05-06)
Some checks failed
CI / validate (push) Failing after 0s
Some checks failed
CI / validate (push) Failing after 0s
This commit is contained in:
commit
004e9ed5d7
5
.github/workflows/ci.yml
vendored
Normal file
5
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
validate:
|
||||
uses: Molecule-AI/molecule-ci/.github/workflows/validate-plugin.yml@main
|
||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Credentials — never commit. Use .env.example as the template.
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.sample
|
||||
|
||||
# Private keys + certs
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# Secret directories
|
||||
.secrets/
|
||||
|
||||
# Workspace auth tokens
|
||||
.auth-token
|
||||
.auth_token
|
||||
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# molecule-freeze-scope
|
||||
|
||||
Molecule AI plugin. Install via the Molecule AI platform plugin system.
|
||||
|
||||
## Usage
|
||||
|
||||
### In org template (org.yaml)
|
||||
```yaml
|
||||
plugins:
|
||||
- molecule-freeze-scope
|
||||
```
|
||||
|
||||
### From URL (community install)
|
||||
```
|
||||
github://Molecule-AI/molecule-ai-plugin-molecule-freeze-scope
|
||||
```
|
||||
|
||||
## License
|
||||
Business Source License 1.1 — © Molecule AI.
|
||||
0
adapters/__init__.py
Normal file
0
adapters/__init__.py
Normal file
2
adapters/claude_code.py
Normal file
2
adapters/claude_code.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Claude Code adaptor — uses the generic rule+skill+hooks installer."""
|
||||
from plugins_registry.builtins import AgentskillsAdaptor as Adaptor # noqa: F401
|
||||
46
hooks/_lib.py
Executable file
46
hooks/_lib.py
Executable file
@ -0,0 +1,46 @@
|
||||
"""Common helpers for Claude Code hooks. Imported by the .py hook scripts.
|
||||
|
||||
Hooks receive JSON on stdin per the Claude Code hook spec, and may emit
|
||||
JSON on stdout or exit with code 2 to block. This module wraps both.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def read_input() -> dict:
|
||||
"""Parse stdin JSON. Empty input → empty dict."""
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def emit(payload: dict) -> None:
|
||||
"""Print JSON payload to stdout for the harness to interpret."""
|
||||
print(json.dumps(payload))
|
||||
|
||||
|
||||
def deny_pretooluse(reason: str) -> None:
|
||||
"""Emit a PreToolUse denial with reason and exit 0."""
|
||||
emit({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason,
|
||||
}
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def add_context(text: str) -> None:
|
||||
"""Emit additionalContext for SessionStart / UserPromptSubmit hooks."""
|
||||
if text and text.strip():
|
||||
emit({"additionalContext": text})
|
||||
|
||||
|
||||
def warn_to_stderr(msg: str) -> None:
|
||||
"""Non-blocking warning visible to the next agent turn via stderr."""
|
||||
print(msg, file=sys.stderr)
|
||||
43
hooks/pre-edit-freeze.py
Executable file
43
hooks/pre-edit-freeze.py
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PreToolUse:Edit/Write — enforce /freeze scope from .claude/freeze."""
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _lib import read_input, deny_pretooluse, warn_to_stderr # noqa
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
FREEZE = os.path.join(REPO, ".claude", "freeze")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not os.path.isfile(FREEZE):
|
||||
return
|
||||
with open(FREEZE) as f:
|
||||
allowed = f.readline().strip()
|
||||
if not allowed:
|
||||
return
|
||||
|
||||
data = read_input()
|
||||
target = data.get("tool_input", {}).get("file_path") or data.get("tool_input", {}).get("notebook_path") or ""
|
||||
if not target:
|
||||
return
|
||||
|
||||
# Always allow .claude/ writes (so unfreeze still works)
|
||||
if "/.claude/" in target or target.endswith("/.claude") or "/.claude" in target:
|
||||
return
|
||||
|
||||
if allowed in target:
|
||||
return
|
||||
|
||||
deny_pretooluse(
|
||||
f"freeze: edit to {target} refused — scope locked to '{allowed}'. "
|
||||
f"Remove .claude/freeze to unlock."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
warn_to_stderr(f"[freeze hook error] {e}")
|
||||
sys.exit(0)
|
||||
2
hooks/pre-edit-freeze.sh
Executable file
2
hooks/pre-edit-freeze.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
exec python3 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pre-edit-freeze.py"
|
||||
11
plugin.yaml
Normal file
11
plugin.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
name: molecule-freeze-scope
|
||||
version: 1.0.0
|
||||
description: Lock edits to a single path glob via .claude/freeze. PreToolUse:Edit/Write hook.
|
||||
author: Molecule AI
|
||||
tags: [molecule, guardrails]
|
||||
|
||||
runtimes:
|
||||
- claude_code
|
||||
|
||||
hooks:
|
||||
- pre-edit-freeze
|
||||
1
settings-fragment.json
Normal file
1
settings-fragment.json
Normal file
@ -0,0 +1 @@
|
||||
{"hooks":{"PreToolUse":[{"matcher":"Edit|Write|NotebookEdit","hooks":[{"type":"command","command":"bash ${CLAUDE_DIR}/hooks/pre-edit-freeze.sh"}]}]}}
|
||||
Loading…
Reference in New Issue
Block a user