opencode-block 1.1.14 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -0
- package/index.ts +134 -134
- package/package.json +28 -28
- package/protect_directories.py +968 -968
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Block
|
|
2
|
+
|
|
3
|
+
**Protect files from unwanted AI modifications in [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [OpenCode](https://opencode.ai).**
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Why use this?
|
|
8
|
+
|
|
9
|
+
- **Prevent accidents** — Stop Claude from touching lock files, CI workflows, or database migrations
|
|
10
|
+
- **Scope to features** — Keep Claude focused on relevant directories, not unrelated code
|
|
11
|
+
- **Guide Claude** — Custom messages explain why files are protected and what to do instead
|
|
12
|
+
- **Zero friction** — Once set up, protection works automatically on every session
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- **Python 3.8+** — Required for the protection hook
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Claude Code
|
|
21
|
+
|
|
22
|
+
1. Register the marketplace:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
/plugin marketplace add kodroi/block-marketplace
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Install the plugin:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/plugin install block@block-marketplace
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### OpenCode
|
|
35
|
+
|
|
36
|
+
Add the plugin to your `opencode.json` config:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"plugins": ["opencode-block"]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or for local development, clone this repo and reference the plugin directly:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"plugins": ["file:///path/to/block/opencode/index.ts"]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
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`:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
your-project/
|
|
56
|
+
├── .opencode/
|
|
57
|
+
│ └── plugin/
|
|
58
|
+
│ └── index.ts # copied from opencode/index.ts
|
|
59
|
+
├── hooks/
|
|
60
|
+
│ └── protect_directories.py # copied from hooks/protect_directories.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **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.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Use the `/block:create` command to interactively create a `.block` file:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/block:create
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or create a `.block` file manually in any directory you want to protect.
|
|
74
|
+
|
|
75
|
+
## .block Format
|
|
76
|
+
|
|
77
|
+
The `.block` file uses JSON format with three modes:
|
|
78
|
+
|
|
79
|
+
### Block All (Default)
|
|
80
|
+
|
|
81
|
+
Empty file or `{}` blocks all modifications:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Allowed List
|
|
88
|
+
|
|
89
|
+
Only allow specific patterns, block everything else:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"allowed": ["*.test.ts", "tests/**/*", "docs/*.md"]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Blocked List
|
|
98
|
+
|
|
99
|
+
Block specific patterns, allow everything else:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"blocked": ["*.lock", "package-lock.json", "migrations/**/*", ".github/**/*"]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Guide Messages
|
|
108
|
+
|
|
109
|
+
Add a message shown when Claude tries to modify protected files:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"blocked": ["migrations/**/*"],
|
|
114
|
+
"guide": "Database migrations are protected. Ask before modifying."
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Pattern-Specific Guides
|
|
119
|
+
|
|
120
|
+
Different messages for different patterns:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"blocked": [
|
|
125
|
+
{ "pattern": "*.lock", "guide": "Lock files are auto-generated. Run the package manager instead." },
|
|
126
|
+
{ "pattern": ".github/**/*", "guide": "CI workflows need manual review." }
|
|
127
|
+
],
|
|
128
|
+
"guide": "This directory has protected files."
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Scope to Feature
|
|
133
|
+
|
|
134
|
+
Keep Claude focused on specific directories during feature work:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"allowed": ["src/features/auth/**/*", "src/components/auth/**/*", "tests/auth/**/*"],
|
|
139
|
+
"guide": "Working on auth feature. Only touching auth-related files."
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Pattern Syntax
|
|
144
|
+
|
|
145
|
+
| Pattern | Description |
|
|
146
|
+
|---------|-------------|
|
|
147
|
+
| `*` | Matches any characters except path separator |
|
|
148
|
+
| `**` | Matches any characters including path separator (recursive) |
|
|
149
|
+
| `?` | Matches single character |
|
|
150
|
+
|
|
151
|
+
### Examples
|
|
152
|
+
|
|
153
|
+
| Pattern | Matches |
|
|
154
|
+
|---------|---------|
|
|
155
|
+
| `*.ts` | All TypeScript files in the directory |
|
|
156
|
+
| `**/*.ts` | All TypeScript files recursively |
|
|
157
|
+
| `src/**/*` | Everything under src/ |
|
|
158
|
+
| `*.test.*` | Files with .test. in the name |
|
|
159
|
+
| `config?.json` | config1.json, configA.json, etc. |
|
|
160
|
+
|
|
161
|
+
## Local Configuration Files
|
|
162
|
+
|
|
163
|
+
For personal or machine-specific protection rules that shouldn't be committed to git, use `.block.local`:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"blocked": [".env.local", ".env.*.local", "appsettings.Development.json"]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Add `.block.local` to your `.gitignore`.
|
|
172
|
+
|
|
173
|
+
When both files exist in the same directory:
|
|
174
|
+
- Blocked patterns are combined (union)
|
|
175
|
+
- Allowed patterns and guide messages use local file
|
|
176
|
+
- Cannot mix `allowed` and `blocked` modes between files
|
|
177
|
+
|
|
178
|
+
## How It Works
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
- **Claude Code**: Uses a PreToolUse hook to intercept Edit, Write, NotebookEdit, and Bash tools
|
|
183
|
+
- **OpenCode**: Uses a `tool.execute.before` hook to intercept edit, write, bash, and patch tools
|
|
184
|
+
|
|
185
|
+
- `.block` files themselves are always protected
|
|
186
|
+
- Protection cascades to all subdirectories
|
|
187
|
+
|
|
188
|
+
### Hierarchical Inheritance
|
|
189
|
+
|
|
190
|
+
When multiple `.block` files exist in the directory hierarchy:
|
|
191
|
+
|
|
192
|
+
**Blocked patterns are combined (union)**:
|
|
193
|
+
```
|
|
194
|
+
project/
|
|
195
|
+
├── .block → {"blocked": ["*.log", "*.tmp"]}
|
|
196
|
+
└── src/
|
|
197
|
+
└── .block → {"blocked": ["generated/**"]}
|
|
198
|
+
```
|
|
199
|
+
Files in `src/` are blocked if they match ANY pattern from either file (`*.log`, `*.tmp`, OR `generated/**`).
|
|
200
|
+
|
|
201
|
+
**Allowed patterns override completely**:
|
|
202
|
+
```
|
|
203
|
+
project/
|
|
204
|
+
├── .block → {"blocked": ["*.lock"]}
|
|
205
|
+
└── features/
|
|
206
|
+
└── .block → {"allowed": ["*.ts"]}
|
|
207
|
+
```
|
|
208
|
+
The `allowed` in `features/` completely overrides the parent — only `*.ts` files can be modified.
|
|
209
|
+
|
|
210
|
+
**Guide messages from the closest file take precedence**:
|
|
211
|
+
When files are blocked by an inherited pattern, the guide message from the closest `.block` file is shown.
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
### Running Tests
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Install dev dependencies
|
|
219
|
+
pip install -e ".[dev]"
|
|
220
|
+
|
|
221
|
+
# Run tests
|
|
222
|
+
pytest tests/ -v
|
|
223
|
+
|
|
224
|
+
# Run with coverage
|
|
225
|
+
pytest tests/ -v --cov=hooks --cov-report=term-missing
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Project Structure
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
block/
|
|
232
|
+
├── hooks/
|
|
233
|
+
│ ├── protect_directories.py # Main protection logic (Python)
|
|
234
|
+
│ └── run-hook.cmd # Cross-platform entry point (Claude Code)
|
|
235
|
+
├── opencode/
|
|
236
|
+
│ ├── index.ts # OpenCode plugin entry point
|
|
237
|
+
│ └── package.json # npm package metadata
|
|
238
|
+
├── tests/
|
|
239
|
+
│ ├── conftest.py # Shared fixtures
|
|
240
|
+
│ ├── test_basic_protection.py
|
|
241
|
+
│ ├── test_allowed_patterns.py
|
|
242
|
+
│ ├── test_blocked_patterns.py
|
|
243
|
+
│ ├── test_guide_messages.py
|
|
244
|
+
│ ├── test_local_config.py
|
|
245
|
+
│ ├── test_invalid_config.py
|
|
246
|
+
│ ├── test_marker_file_protection.py
|
|
247
|
+
│ ├── test_tool_types.py
|
|
248
|
+
│ ├── test_bash_commands.py
|
|
249
|
+
│ ├── test_wildcards.py
|
|
250
|
+
│ └── test_edge_cases.py
|
|
251
|
+
├── commands/
|
|
252
|
+
│ └── create.md # Interactive command (Claude Code)
|
|
253
|
+
├── .claude-plugin/
|
|
254
|
+
│ └── plugin.json # Plugin metadata (Claude Code)
|
|
255
|
+
└── pyproject.toml # Python project config
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|
package/index.ts
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Block plugin for OpenCode
|
|
3
|
-
*
|
|
4
|
-
* Provides file and directory protection using .block configuration files.
|
|
5
|
-
* Intercepts file modification tools (edit, write, bash, patch) and blocks
|
|
6
|
-
* them based on protection rules defined in .block files.
|
|
7
|
-
*
|
|
8
|
-
* This is the OpenCode equivalent of the Claude Code PreToolUse hook.
|
|
9
|
-
*/
|
|
10
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
11
|
-
import { resolve } from "path";
|
|
12
|
-
|
|
13
|
-
/** Tools that modify files and should be checked against .block rules. */
|
|
14
|
-
const PROTECTED_TOOLS = new Set(["edit", "write", "bash", "patch"]);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Maps OpenCode tool names to the names expected by protect_directories.py.
|
|
18
|
-
* The Python script was originally written for Claude Code's tool naming.
|
|
19
|
-
*/
|
|
20
|
-
const TOOL_NAME_MAP: Record<string, string> = {
|
|
21
|
-
edit: "Edit",
|
|
22
|
-
write: "Write",
|
|
23
|
-
bash: "Bash",
|
|
24
|
-
patch: "Write",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Build the JSON input that protect_directories.py expects on stdin.
|
|
29
|
-
*
|
|
30
|
-
* Claude Code hook input format:
|
|
31
|
-
* { "tool_name": "Edit", "tool_input": { "file_path": "..." } }
|
|
32
|
-
* { "tool_name": "Bash", "tool_input": { "command": "..." } }
|
|
33
|
-
*
|
|
34
|
-
* OpenCode uses camelCase args: filePath for edit/write/patch, command for bash.
|
|
35
|
-
*/
|
|
36
|
-
function buildHookInput(
|
|
37
|
-
tool: string,
|
|
38
|
-
args: Record<string, unknown>,
|
|
39
|
-
): string | null {
|
|
40
|
-
const toolName = TOOL_NAME_MAP[tool];
|
|
41
|
-
if (!toolName) return null;
|
|
42
|
-
|
|
43
|
-
const toolInput: Record<string, unknown> = {};
|
|
44
|
-
|
|
45
|
-
if (tool === "bash") {
|
|
46
|
-
if (!args.command) return null;
|
|
47
|
-
toolInput.command = args.command;
|
|
48
|
-
} else {
|
|
49
|
-
// edit, write, patch — OpenCode provides the path as "filePath"
|
|
50
|
-
if (!args.filePath) return null;
|
|
51
|
-
toolInput.file_path = args.filePath;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return JSON.stringify({ tool_name: toolName, tool_input: toolInput });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Locate protect_directories.py relative to this plugin file.
|
|
59
|
-
*
|
|
60
|
-
* NOTE: import.meta.dir is a Bun-specific API. OpenCode runs plugins via Bun,
|
|
61
|
-
* so this is safe. Packages installed via npm are cached under
|
|
62
|
-
* ~/.cache/opencode/node_modules/.
|
|
63
|
-
*
|
|
64
|
-
* When installed via npm the layout is:
|
|
65
|
-
* node_modules/opencode-block/protect_directories.py (copied by prepack)
|
|
66
|
-
* node_modules/opencode-block/index.ts
|
|
67
|
-
*
|
|
68
|
-
* When used from the repo directly:
|
|
69
|
-
* opencode/index.ts
|
|
70
|
-
* hooks/protect_directories.py
|
|
71
|
-
*/
|
|
72
|
-
function findScript(): string {
|
|
73
|
-
const pluginDir = import.meta.dir;
|
|
74
|
-
// npm-installed: protect_directories.py is copied alongside index.ts
|
|
75
|
-
const colocated = resolve(pluginDir, "protect_directories.py");
|
|
76
|
-
try {
|
|
77
|
-
const fs = require("fs");
|
|
78
|
-
if (fs.existsSync(colocated)) return colocated;
|
|
79
|
-
} catch {
|
|
80
|
-
// Fall through to repo layout
|
|
81
|
-
}
|
|
82
|
-
// Repo layout: ../hooks/protect_directories.py
|
|
83
|
-
return resolve(pluginDir, "..", "hooks", "protect_directories.py");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export const BlockPlugin: Plugin = async ({ $ }) => {
|
|
87
|
-
const scriptPath = findScript();
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
"tool.execute.before": async (input, output) => {
|
|
91
|
-
if (!PROTECTED_TOOLS.has(input.tool)) return;
|
|
92
|
-
|
|
93
|
-
const hookInput = buildHookInput(
|
|
94
|
-
input.tool,
|
|
95
|
-
output.args as Record<string, unknown>,
|
|
96
|
-
);
|
|
97
|
-
if (!hookInput) return;
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const result =
|
|
101
|
-
await $`echo ${hookInput} | python3 ${scriptPath}`.quiet();
|
|
102
|
-
const stdout = result.stdout.toString().trim();
|
|
103
|
-
if (!stdout) return;
|
|
104
|
-
|
|
105
|
-
const decision = JSON.parse(stdout);
|
|
106
|
-
if (decision.decision === "block") {
|
|
107
|
-
throw new Error(decision.reason);
|
|
108
|
-
}
|
|
109
|
-
} catch (err: unknown) {
|
|
110
|
-
if (err instanceof SyntaxError) {
|
|
111
|
-
// Python output wasn't JSON — not a block, ignore
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
// Block errors from our own throw above — re-throw
|
|
115
|
-
if (err instanceof Error && err.message) {
|
|
116
|
-
const msg = err.message;
|
|
117
|
-
// Infrastructure failures (python3 not found, spawn errors) should
|
|
118
|
-
// not prevent the operation — log a warning and let it proceed.
|
|
119
|
-
if (
|
|
120
|
-
(err as NodeJS.ErrnoException).code === "ENOENT" ||
|
|
121
|
-
msg.includes("not found") ||
|
|
122
|
-
msg.includes("No such file") ||
|
|
123
|
-
msg.includes("python3")
|
|
124
|
-
) {
|
|
125
|
-
console.warn(`[block] Protection check skipped: ${msg}`);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Re-throw actual block errors and other unexpected failures
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Block plugin for OpenCode
|
|
3
|
+
*
|
|
4
|
+
* Provides file and directory protection using .block configuration files.
|
|
5
|
+
* Intercepts file modification tools (edit, write, bash, patch) and blocks
|
|
6
|
+
* them based on protection rules defined in .block files.
|
|
7
|
+
*
|
|
8
|
+
* This is the OpenCode equivalent of the Claude Code PreToolUse hook.
|
|
9
|
+
*/
|
|
10
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
|
|
13
|
+
/** Tools that modify files and should be checked against .block rules. */
|
|
14
|
+
const PROTECTED_TOOLS = new Set(["edit", "write", "bash", "patch"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps OpenCode tool names to the names expected by protect_directories.py.
|
|
18
|
+
* The Python script was originally written for Claude Code's tool naming.
|
|
19
|
+
*/
|
|
20
|
+
const TOOL_NAME_MAP: Record<string, string> = {
|
|
21
|
+
edit: "Edit",
|
|
22
|
+
write: "Write",
|
|
23
|
+
bash: "Bash",
|
|
24
|
+
patch: "Write",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the JSON input that protect_directories.py expects on stdin.
|
|
29
|
+
*
|
|
30
|
+
* Claude Code hook input format:
|
|
31
|
+
* { "tool_name": "Edit", "tool_input": { "file_path": "..." } }
|
|
32
|
+
* { "tool_name": "Bash", "tool_input": { "command": "..." } }
|
|
33
|
+
*
|
|
34
|
+
* OpenCode uses camelCase args: filePath for edit/write/patch, command for bash.
|
|
35
|
+
*/
|
|
36
|
+
function buildHookInput(
|
|
37
|
+
tool: string,
|
|
38
|
+
args: Record<string, unknown>,
|
|
39
|
+
): string | null {
|
|
40
|
+
const toolName = TOOL_NAME_MAP[tool];
|
|
41
|
+
if (!toolName) return null;
|
|
42
|
+
|
|
43
|
+
const toolInput: Record<string, unknown> = {};
|
|
44
|
+
|
|
45
|
+
if (tool === "bash") {
|
|
46
|
+
if (!args.command) return null;
|
|
47
|
+
toolInput.command = args.command;
|
|
48
|
+
} else {
|
|
49
|
+
// edit, write, patch — OpenCode provides the path as "filePath"
|
|
50
|
+
if (!args.filePath) return null;
|
|
51
|
+
toolInput.file_path = args.filePath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return JSON.stringify({ tool_name: toolName, tool_input: toolInput });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Locate protect_directories.py relative to this plugin file.
|
|
59
|
+
*
|
|
60
|
+
* NOTE: import.meta.dir is a Bun-specific API. OpenCode runs plugins via Bun,
|
|
61
|
+
* so this is safe. Packages installed via npm are cached under
|
|
62
|
+
* ~/.cache/opencode/node_modules/.
|
|
63
|
+
*
|
|
64
|
+
* When installed via npm the layout is:
|
|
65
|
+
* node_modules/opencode-block/protect_directories.py (copied by prepack)
|
|
66
|
+
* node_modules/opencode-block/index.ts
|
|
67
|
+
*
|
|
68
|
+
* When used from the repo directly:
|
|
69
|
+
* opencode/index.ts
|
|
70
|
+
* hooks/protect_directories.py
|
|
71
|
+
*/
|
|
72
|
+
function findScript(): string {
|
|
73
|
+
const pluginDir = import.meta.dir;
|
|
74
|
+
// npm-installed: protect_directories.py is copied alongside index.ts
|
|
75
|
+
const colocated = resolve(pluginDir, "protect_directories.py");
|
|
76
|
+
try {
|
|
77
|
+
const fs = require("fs");
|
|
78
|
+
if (fs.existsSync(colocated)) return colocated;
|
|
79
|
+
} catch {
|
|
80
|
+
// Fall through to repo layout
|
|
81
|
+
}
|
|
82
|
+
// Repo layout: ../hooks/protect_directories.py
|
|
83
|
+
return resolve(pluginDir, "..", "hooks", "protect_directories.py");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const BlockPlugin: Plugin = async ({ $ }) => {
|
|
87
|
+
const scriptPath = findScript();
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"tool.execute.before": async (input, output) => {
|
|
91
|
+
if (!PROTECTED_TOOLS.has(input.tool)) return;
|
|
92
|
+
|
|
93
|
+
const hookInput = buildHookInput(
|
|
94
|
+
input.tool,
|
|
95
|
+
output.args as Record<string, unknown>,
|
|
96
|
+
);
|
|
97
|
+
if (!hookInput) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result =
|
|
101
|
+
await $`echo ${hookInput} | python3 ${scriptPath}`.quiet();
|
|
102
|
+
const stdout = result.stdout.toString().trim();
|
|
103
|
+
if (!stdout) return;
|
|
104
|
+
|
|
105
|
+
const decision = JSON.parse(stdout);
|
|
106
|
+
if (decision.decision === "block") {
|
|
107
|
+
throw new Error(decision.reason);
|
|
108
|
+
}
|
|
109
|
+
} catch (err: unknown) {
|
|
110
|
+
if (err instanceof SyntaxError) {
|
|
111
|
+
// Python output wasn't JSON — not a block, ignore
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Block errors from our own throw above — re-throw
|
|
115
|
+
if (err instanceof Error && err.message) {
|
|
116
|
+
const msg = err.message;
|
|
117
|
+
// Infrastructure failures (python3 not found, spawn errors) should
|
|
118
|
+
// not prevent the operation — log a warning and let it proceed.
|
|
119
|
+
if (
|
|
120
|
+
(err as NodeJS.ErrnoException).code === "ENOENT" ||
|
|
121
|
+
msg.includes("not found") ||
|
|
122
|
+
msg.includes("No such file") ||
|
|
123
|
+
msg.includes("python3")
|
|
124
|
+
) {
|
|
125
|
+
console.warn(`[block] Protection check skipped: ${msg}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Re-throw actual block errors and other unexpected failures
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
};
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "opencode-block",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "File and directory protection for OpenCode using .block marker files with pattern matching",
|
|
5
|
-
"main": "index.ts",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"prepack": "node -e \"require('fs').copyFileSync('../hooks/protect_directories.py','protect_directories.py')\"",
|
|
8
|
-
"postpack": "node -e \"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"opencode",
|
|
12
|
-
"opencode-plugin",
|
|
13
|
-
"protection",
|
|
14
|
-
"security",
|
|
15
|
-
"file-blocking",
|
|
16
|
-
"directory-lock"
|
|
17
|
-
],
|
|
18
|
-
"author": "Iiro Rahkonen",
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "https://github.com/kodroi/block"
|
|
23
|
-
},
|
|
24
|
-
"files": [
|
|
25
|
-
"index.ts",
|
|
26
|
-
"protect_directories.py"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-block",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "File and directory protection for OpenCode using .block marker files with pattern matching",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prepack": "node -e \"const fs=require('fs');fs.copyFileSync('../hooks/protect_directories.py','protect_directories.py');fs.copyFileSync('../README.md','README.md')\"",
|
|
8
|
+
"postpack": "node -e \"const fs=require('fs');['protect_directories.py','README.md'].forEach(f=>{try{fs.unlinkSync(f)}catch(e){}})\""
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"opencode",
|
|
12
|
+
"opencode-plugin",
|
|
13
|
+
"protection",
|
|
14
|
+
"security",
|
|
15
|
+
"file-blocking",
|
|
16
|
+
"directory-lock"
|
|
17
|
+
],
|
|
18
|
+
"author": "Iiro Rahkonen",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/kodroi/block"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"index.ts",
|
|
26
|
+
"protect_directories.py"
|
|
27
|
+
]
|
|
28
|
+
}
|