From 004e9ed5d77b47b7da5a96161d660d7a9f7d8615 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 6 May 2026 13:53:27 -0700 Subject: [PATCH] import from local vendored copy (2026-05-06) --- .github/workflows/ci.yml | 5 +++++ .gitignore | 21 ++++++++++++++++++ README.md | 19 +++++++++++++++++ adapters/__init__.py | 0 adapters/claude_code.py | 2 ++ hooks/_lib.py | 46 ++++++++++++++++++++++++++++++++++++++++ hooks/pre-edit-freeze.py | 43 +++++++++++++++++++++++++++++++++++++ hooks/pre-edit-freeze.sh | 2 ++ plugin.yaml | 11 ++++++++++ settings-fragment.json | 1 + 10 files changed, 150 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 adapters/__init__.py create mode 100644 adapters/claude_code.py create mode 100755 hooks/_lib.py create mode 100755 hooks/pre-edit-freeze.py create mode 100755 hooks/pre-edit-freeze.sh create mode 100644 plugin.yaml create mode 100644 settings-fragment.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c8fb9d3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,5 @@ +name: CI +on: [push, pull_request] +jobs: + validate: + uses: Molecule-AI/molecule-ci/.github/workflows/validate-plugin.yml@main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2af45b5 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..44d525e --- /dev/null +++ b/README.md @@ -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. diff --git a/adapters/__init__.py b/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adapters/claude_code.py b/adapters/claude_code.py new file mode 100644 index 0000000..cc58993 --- /dev/null +++ b/adapters/claude_code.py @@ -0,0 +1,2 @@ +"""Claude Code adaptor — uses the generic rule+skill+hooks installer.""" +from plugins_registry.builtins import AgentskillsAdaptor as Adaptor # noqa: F401 diff --git a/hooks/_lib.py b/hooks/_lib.py new file mode 100755 index 0000000..1d0555a --- /dev/null +++ b/hooks/_lib.py @@ -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) diff --git a/hooks/pre-edit-freeze.py b/hooks/pre-edit-freeze.py new file mode 100755 index 0000000..a1a9d33 --- /dev/null +++ b/hooks/pre-edit-freeze.py @@ -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) diff --git a/hooks/pre-edit-freeze.sh b/hooks/pre-edit-freeze.sh new file mode 100755 index 0000000..3ad5ce3 --- /dev/null +++ b/hooks/pre-edit-freeze.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec python3 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pre-edit-freeze.py" diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..ea71e1f --- /dev/null +++ b/plugin.yaml @@ -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 diff --git a/settings-fragment.json b/settings-fragment.json new file mode 100644 index 0000000..2a2895d --- /dev/null +++ b/settings-fragment.json @@ -0,0 +1 @@ +{"hooks":{"PreToolUse":[{"matcher":"Edit|Write|NotebookEdit","hooks":[{"type":"command","command":"bash ${CLAUDE_DIR}/hooks/pre-edit-freeze.sh"}]}]}}