-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add OpenCode plugin support #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2d51a36
c355fb8
0ee98e8
e32172c
918b3f7
65cf87f
295f648
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| # Block | ||
|
|
||
| **A Claude Code plugin to protect files from unwanted modifications.** | ||
| **Protect files from unwanted AI modifications in [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [OpenCode](https://opencode.ai).** | ||
|
|
||
| Drop a `.block` file in any directory to control what Claude can and cannot edit. Protect configs, lock files, migrations, or entire directories with simple pattern rules. | ||
| Drop a `.block` file in any directory to control what AI agents can and cannot edit. Protect configs, lock files, migrations, or entire directories with simple pattern rules. | ||
|
|
||
| ## Why use this? | ||
|
|
||
|
|
@@ -17,6 +17,8 @@ Drop a `.block` file in any directory to control what Claude can and cannot edit | |
|
|
||
| ## Installation | ||
|
|
||
| ### Claude Code | ||
|
|
||
| 1. Register the marketplace: | ||
|
|
||
| ``` | ||
|
|
@@ -29,6 +31,37 @@ Drop a `.block` file in any directory to control what Claude can and cannot edit | |
| /plugin install block@block-marketplace | ||
| ``` | ||
|
|
||
| ### OpenCode | ||
|
|
||
| Add the plugin to your `opencode.json` config: | ||
|
|
||
| ```json | ||
| { | ||
| "plugins": ["opencode-block"] | ||
| } | ||
| ``` | ||
|
|
||
| Or for local development, clone this repo and reference the plugin directly: | ||
|
|
||
| ```json | ||
| { | ||
| "plugins": ["file:///path/to/block/opencode/index.ts"] | ||
| } | ||
| ``` | ||
|
|
||
| You can also set up the plugin manually by copying files into your project. The plugin expects `hooks/protect_directories.py` to be a sibling of the directory containing `index.ts`: | ||
|
|
||
| ``` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a language specifier to the fenced code block (MD040). The directory-tree code block on line 54 is missing a language tag; use 📝 Proposed fix-```
+```text
your-project/🧰 Tools🪛 markdownlint-cli2 (0.21.0)[warning] 54-54: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
| your-project/ | ||
| ├── .opencode/ | ||
| │ └── plugin/ | ||
| │ └── index.ts # copied from opencode/index.ts | ||
| ├── hooks/ | ||
| │ └── protect_directories.py # copied from hooks/protect_directories.py | ||
| ``` | ||
|
|
||
| > **Note:** The `tool.execute.before` hook protects tools called by the primary agent. Tools invoked by subagents spawned via the `task` tool may not be intercepted. | ||
|
|
||
| ## Usage | ||
|
|
||
| Use the `/block:create` command to interactively create a `.block` file: | ||
|
|
@@ -144,7 +177,10 @@ When both files exist in the same directory: | |
|
|
||
| ## How It Works | ||
|
|
||
| The plugin hooks into Claude's file operations. When Claude tries to modify a file, it checks for `.block` files in the target directory and all parent directories, then combines their rules. | ||
| The plugin hooks into file operations from Claude Code and OpenCode. When the AI agent tries to modify a file, the plugin checks for `.block` files in the target directory and all parent directories, then combines their rules. | ||
|
|
||
| - **Claude Code**: Uses a PreToolUse hook to intercept Edit, Write, NotebookEdit, and Bash tools | ||
| - **OpenCode**: Uses a `tool.execute.before` hook to intercept edit, write, bash, and patch tools | ||
|
|
||
| - `.block` files themselves are always protected | ||
| - Protection cascades to all subdirectories | ||
|
|
@@ -194,8 +230,11 @@ pytest tests/ -v --cov=hooks --cov-report=term-missing | |
| ``` | ||
| block/ | ||
| ├── hooks/ | ||
| │ ├── protect_directories.py # Main protection logic | ||
| │ └── run-hook.cmd # Cross-platform entry point | ||
| │ ├── protect_directories.py # Main protection logic (Python) | ||
| │ └── run-hook.cmd # Cross-platform entry point (Claude Code) | ||
| ├── opencode/ | ||
| │ ├── index.ts # OpenCode plugin entry point | ||
| │ └── package.json # npm package metadata | ||
| ├── tests/ | ||
| │ ├── conftest.py # Shared fixtures | ||
| │ ├── test_basic_protection.py | ||
|
|
@@ -210,9 +249,9 @@ block/ | |
| │ ├── test_wildcards.py | ||
| │ └── test_edge_cases.py | ||
| ├── commands/ | ||
| │ └── create.md # Interactive command | ||
| │ └── create.md # Interactive command (Claude Code) | ||
| ├── .claude-plugin/ | ||
| │ └── plugin.json # Plugin metadata | ||
| │ └── plugin.json # Plugin metadata (Claude Code) | ||
| └── pyproject.toml # Python project config | ||
| ``` | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /** | ||
| * Block plugin for OpenCode | ||
| * | ||
| * Provides file and directory protection using .block configuration files. | ||
| * Intercepts file modification tools (edit, write, bash, patch) and blocks | ||
| * them based on protection rules defined in .block files. | ||
| * | ||
| * This is the OpenCode equivalent of the Claude Code PreToolUse hook. | ||
| */ | ||
| import type { Plugin } from "@opencode-ai/plugin"; | ||
| import { resolve } from "path"; | ||
|
|
||
| /** Tools that modify files and should be checked against .block rules. */ | ||
| const PROTECTED_TOOLS = new Set(["edit", "write", "bash", "patch"]); | ||
|
|
||
| /** | ||
| * Maps OpenCode tool names to the names expected by protect_directories.py. | ||
| * The Python script was originally written for Claude Code's tool naming. | ||
| */ | ||
| const TOOL_NAME_MAP: Record<string, string> = { | ||
| edit: "Edit", | ||
| write: "Write", | ||
| bash: "Bash", | ||
| patch: "Write", | ||
| }; | ||
|
|
||
| /** | ||
| * Build the JSON input that protect_directories.py expects on stdin. | ||
| * | ||
| * Claude Code hook input format: | ||
| * { "tool_name": "Edit", "tool_input": { "file_path": "..." } } | ||
| * { "tool_name": "Bash", "tool_input": { "command": "..." } } | ||
| * | ||
| * OpenCode uses camelCase args: filePath for edit/write/patch, command for bash. | ||
| */ | ||
| function buildHookInput( | ||
| tool: string, | ||
| args: Record<string, unknown>, | ||
| ): string | null { | ||
| const toolName = TOOL_NAME_MAP[tool]; | ||
| if (!toolName) return null; | ||
|
|
||
| const toolInput: Record<string, unknown> = {}; | ||
|
|
||
| if (tool === "bash") { | ||
| if (!args.command) return null; | ||
| toolInput.command = args.command; | ||
| } else { | ||
| // edit, write, patch — OpenCode provides the path as "filePath" | ||
| if (!args.filePath) return null; | ||
| toolInput.file_path = args.filePath; | ||
| } | ||
|
|
||
| return JSON.stringify({ tool_name: toolName, tool_input: toolInput }); | ||
| } | ||
|
|
||
| /** | ||
| * Locate protect_directories.py relative to this plugin file. | ||
| * | ||
| * NOTE: import.meta.dir is a Bun-specific API. OpenCode runs plugins via Bun, | ||
| * so this is safe. Packages installed via npm are cached under | ||
| * ~/.cache/opencode/node_modules/. | ||
| * | ||
| * When installed via npm the layout is: | ||
| * node_modules/opencode-block/protect_directories.py (copied by prepack) | ||
| * node_modules/opencode-block/index.ts | ||
| * | ||
| * When used from the repo directly: | ||
| * opencode/index.ts | ||
| * hooks/protect_directories.py | ||
| */ | ||
| function findScript(): string { | ||
| const pluginDir = import.meta.dir; | ||
| // npm-installed: protect_directories.py is copied alongside index.ts | ||
| const colocated = resolve(pluginDir, "protect_directories.py"); | ||
| try { | ||
| const fs = require("fs"); | ||
| if (fs.existsSync(colocated)) return colocated; | ||
| } catch { | ||
| // Fall through to repo layout | ||
| } | ||
| // Repo layout: ../hooks/protect_directories.py | ||
| return resolve(pluginDir, "..", "hooks", "protect_directories.py"); | ||
| } | ||
|
|
||
| export const BlockPlugin: Plugin = async ({ $ }) => { | ||
| const scriptPath = findScript(); | ||
|
|
||
| return { | ||
| "tool.execute.before": async (input, output) => { | ||
| if (!PROTECTED_TOOLS.has(input.tool)) return; | ||
|
|
||
| const hookInput = buildHookInput( | ||
| input.tool, | ||
| output.args as Record<string, unknown>, | ||
| ); | ||
| if (!hookInput) return; | ||
|
|
||
| try { | ||
| const result = | ||
| await $`echo ${hookInput} | python3 ${scriptPath}`.quiet(); | ||
| const stdout = result.stdout.toString().trim(); | ||
| if (!stdout) return; | ||
|
|
||
| const decision = JSON.parse(stdout); | ||
| if (decision.decision === "block") { | ||
| throw new Error(decision.reason); | ||
| } | ||
| } catch (err: unknown) { | ||
| if (err instanceof SyntaxError) { | ||
| // Python output wasn't JSON — not a block, ignore | ||
| return; | ||
| } | ||
| // Block errors from our own throw above — re-throw | ||
| if (err instanceof Error && err.message) { | ||
| const msg = err.message; | ||
| // Infrastructure failures (python3 not found, spawn errors) should | ||
| // not prevent the operation — log a warning and let it proceed. | ||
| if ( | ||
| (err as NodeJS.ErrnoException).code === "ENOENT" || | ||
| msg.includes("not found") || | ||
| msg.includes("No such file") || | ||
| msg.includes("python3") | ||
| ) { | ||
| console.warn(`[block] Protection check skipped: ${msg}`); | ||
| return; | ||
| } | ||
| } | ||
| // Re-throw actual block errors and other unexpected failures | ||
| throw err; | ||
| } | ||
| }, | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "opencode-block", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "version": "1.1.14", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "description": "File and directory protection for OpenCode using .block marker files with pattern matching", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "main": "index.ts", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "scripts": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "prepack": "node -e \"const fs=require('fs');fs.copyFileSync('../hooks/protect_directories.py','protect_directories.py');fs.copyFileSync('../README.md','README.md')\"", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "postpack": "node -e \"const fs=require('fs');['protect_directories.py','README.md'].forEach(f=>{try{fs.unlinkSync(f)}catch(e){}})\"" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "keywords": [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "opencode", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "opencode-plugin", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "protection", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "security", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "file-blocking", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "directory-lock" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "author": "Iiro Rahkonen", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "license": "MIT", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "repository": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "git", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "url": "https://github.com/kodroi/block" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "files": [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "index.ts", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "protect_directories.py" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's verify the package.json location and content
find . -name "package.json" -path "*/opencode/*" | head -20Repository: kodroi/block Length of output: 79 🏁 Script executed: # Also, let's look for the index.ts file
find . -name "index.ts" -path "*/opencode/*" | head -20Repository: kodroi/block Length of output: 75 🏁 Script executed: # Check the repository structure to understand the layout
git ls-files | grep -E "(opencode|package\.json|index\.ts)" | head -30Repository: kodroi/block Length of output: 95 🏁 Script executed: # Read the index.ts file to verify imports
cat -n ./opencode/index.tsRepository: kodroi/block Length of output: 5285 🏁 Script executed: # Read package.json to verify its current state
cat -n ./opencode/package.jsonRepository: kodroi/block Length of output: 916 🏁 Script executed: # Search for any OpenCode documentation or examples about plugin dependencies
find . -type f -name "*.md" -o -name "*.txt" | xargs grep -l "opencode-ai/plugin\|plugin.*dependency" 2>/dev/null | head -10Repository: kodroi/block Length of output: 38 🏁 Script executed: # Check if there are other plugin examples in the repository
find . -type f -name "package.json" | xargs grep -l "@opencode-ai/plugin\|opencode-plugin" | head -10Repository: kodroi/block Length of output: 79 🏁 Script executed: # Look for any tsconfig or build configuration that might indicate how plugins are compiled
find . -name "tsconfig.json" -o -name "bunfig.toml" -o -name ".bun*" | head -10Repository: kodroi/block Length of output: 38 🏁 Script executed: # Check if `@opencode-ai/plugin` is used elsewhere in the repo
rg "@opencode-ai/plugin" --type ts --type jsonRepository: kodroi/block Length of output: 124 🌐 Web query:
💡 Result: To declare dependencies for an OpenCode plugin (using 1) Local plugin (
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |
| "name": "opencode-block", | |
| "version": "1.1.14", | |
| "description": "File and directory protection for OpenCode using .block marker files with pattern matching", | |
| "main": "index.ts", | |
| "scripts": { | |
| "prepack": "cp ../hooks/protect_directories.py protect_directories.py", | |
| "postpack": "rm -f protect_directories.py" | |
| }, | |
| "keywords": [ | |
| "opencode", | |
| "opencode-plugin", | |
| "protection", | |
| "security", | |
| "file-blocking", | |
| "directory-lock" | |
| ], | |
| "author": "Iiro Rahkonen", | |
| "license": "MIT", | |
| "repository": { | |
| "type": "git", | |
| "url": "https://github.com/kodroi/block" | |
| }, | |
| "files": [ | |
| "index.ts", | |
| "protect_directories.py" | |
| ] | |
| } | |
| { | |
| "name": "opencode-block", | |
| "version": "1.1.14", | |
| "description": "File and directory protection for OpenCode using .block marker files with pattern matching", | |
| "main": "index.ts", | |
| "scripts": { | |
| "prepack": "cp ../hooks/protect_directories.py protect_directories.py", | |
| "postpack": "rm -f protect_directories.py" | |
| }, | |
| "dependencies": { | |
| "@opencode-ai/plugin": "latest" | |
| }, | |
| "keywords": [ | |
| "opencode", | |
| "opencode-plugin", | |
| "protection", | |
| "security", | |
| "file-blocking", | |
| "directory-lock" | |
| ], | |
| "author": "Iiro Rahkonen", | |
| "license": "MIT", | |
| "repository": { | |
| "type": "git", | |
| "url": "https://github.com/kodroi/block" | |
| }, | |
| "files": [ | |
| "index.ts", | |
| "protect_directories.py" | |
| ] | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@opencode/package.json` around lines 1 - 28, The package.json is missing a
dependencies section for the runtime import used in index.ts; add a
"dependencies" object to package.json and declare "@opencode-ai/plugin" with an
appropriate semver (e.g., a caret-range matching the plugin API you built
against) so that when Bun/npm installs this package it also installs
`@opencode-ai/plugin`; update the "files" or other metadata only if necessary, but
the main fix is adding the dependencies entry referencing "@opencode-ai/plugin"
so the import in index.ts resolves at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
OpenCode configuration schema opencode.json plugin key💡 Result:
In OpenCode’s
opencode.json/opencode.jsoncconfig, the key for plugins is:"plugin": an array of plugin identifiers (npm package names, including scoped packages; you can also pin versions likename@x.y.z). (opencode.ai)Example:
{ "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-helicone-session", "opencode-wakatime", "@my-org/custom-plugin"] }(opencode.ai)
Schema URL (for editor validation/autocomplete):
"$schema": "https://opencode.ai/config.json"(opencode.ai)Citations:
🏁 Script executed:
Repository: kodroi/block
Length of output: 150
opencode.jsonconfig key should be"plugin"(singular), not"plugins"(plural).OpenCode's configuration schema uses
"plugin"for the plugins array. The README incorrectly shows"plugins"(plural) in two places (lines 40 and 48), which won't be recognized and the plugin won't load.Proposed fixes
Line 40:
{ - "plugins": ["opencode-block"] + "plugin": ["opencode-block"] }Line 48:
{ - "plugins": ["file:///path/to/block/opencode/index.ts"] + "plugin": ["file:///path/to/block/opencode/index.ts"] }🤖 Prompt for AI Agents