opencode-branch-guard 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hugo Batista
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # opencode-branch-guard
2
+
3
+ [![GitHub Tag](https://img.shields.io/github/v/tag/hugobatista/opencode-branch-guard?logo=github&label=latest)](https://go.hugobatista.com/gh/opencode-branch-guard/releases)
4
+ [![Lint](https://img.shields.io/github/actions/workflow/status/hugobatista/opencode-branch-guard/lint.yml?label=Lint)](https://go.hugobatista.com/gh/opencode-branch-guard/actions/workflows/lint.yml)
5
+ [![Test](https://img.shields.io/github/actions/workflow/status/hugobatista/opencode-branch-guard/test.yml?label=Test)](https://go.hugobatista.com/gh/opencode-branch-guard/actions/workflows/test.yml)
6
+ [![npm](https://img.shields.io/npm/v/opencode-branch-guard.svg)](https://www.npmjs.com/package/opencode-branch-guard)
7
+
8
+ OpenCode plugin. Blocks git mutations (`commit`, `push`, `merge`, `rebase`,
9
+ `reset`, …) based on the current branch or repository, so protected branches stay
10
+ clean. Deny a whole branch, allow a different policy per repository, and keep
11
+ read-only commands untouched.
12
+
13
+ > **Requires OpenCode V2.** OpenCode V2 changed the plugin API; V1 plugin
14
+ > implementations do not run in V2. This plugin is built against
15
+ > `@opencode/plugin` V2 only.
16
+
17
+ ## What it does
18
+
19
+ - Intercepts the `shell` permission via `ctx.permission.hook("evaluate")` and
20
+ returns `effect: "deny"` when a git mutation violates the resolved policy.
21
+ - Resolves the branch with `git -C <directory> branch --show-current`; the
22
+ directory comes from the session (`ctx.session.get().location.directory`), not
23
+ the plugin instance.
24
+ - Resolves the policy hierarchically: `repos[<directory>]` overrides
25
+ `branches[<branch>]`, which overrides `default`.
26
+ - Applies the same decision to every resource of a compound command, so
27
+ `cd /tmp && git commit` is caught.
28
+ - Passes read-only commands (`status`, `log`, `diff`, `fetch`, …) and anything
29
+ that is not a known git mutation.
30
+ - Fails closed: with no options, every git mutation is denied.
31
+
32
+ ## Requirements
33
+
34
+ - **OpenCode V2.** The V2 release changed the plugin API; V1 plugin
35
+ implementations do not run in V2.
36
+ - [Bun](https://bun.sh) to install dependencies (dev only).
37
+
38
+ ## Install
39
+
40
+ ```sh
41
+ opencode plugin add opencode-branch-guard
42
+ ```
43
+
44
+ Or add the package to `opencode.jsonc` (project or
45
+ `~/.config/opencode/opencode.jsonc`):
46
+
47
+ ```jsonc
48
+ {
49
+ "$schema": "https://opencode.ai/config.json",
50
+ "plugins": ["opencode-branch-guard"]
51
+ }
52
+ ```
53
+
54
+ With no options the plugin is **fail-closed**: it denies every git mutation.
55
+ Pass [options](#configuration) to allow the operations you want.
56
+
57
+ > This is a **server** plugin. Configure it in `opencode.json(c)`. The
58
+ > `cli.json` file is for terminal (TUI) plugins only.
59
+
60
+ ### Install from source (local dev)
61
+
62
+ 1. Clone the repository and install dependencies:
63
+
64
+ ```sh
65
+ git clone https://github.com/hugobatista/opencode-branch-guard.git ~/code/projects/opencode-branch-guard
66
+ cd ~/code/projects/opencode-branch-guard
67
+ bun install
68
+ ```
69
+
70
+ 2. Register the plugin in your `opencode.jsonc` with an absolute path to the
71
+ `src` directory (a local plugin directory must contain `index.ts` at its
72
+ root):
73
+
74
+ ```jsonc
75
+ {
76
+ "$schema": "https://opencode.ai/config.json",
77
+ "plugins": ["/home/your-user/code/projects/opencode-branch-guard/src"]
78
+ }
79
+ ```
80
+
81
+ 3. Restart OpenCode.
82
+
83
+ ## Configuration
84
+
85
+ Pass options with the object form:
86
+
87
+ ```jsonc
88
+ {
89
+ "$schema": "https://opencode.ai/config.json",
90
+ "plugins": [
91
+ {
92
+ "package": "opencode-branch-guard",
93
+ "options": {
94
+ "default": {
95
+ "allow": ["add", "branch", "checkout", "commit", "push", "fetch", "merge", "pull", "rebase", "reset", "restore", "stash", "switch", "tag"],
96
+ "deny": []
97
+ },
98
+ "branches": {
99
+ "main": { "allow": [] },
100
+ "master": { "allow": [] }
101
+ }
102
+ }
103
+ }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ ### Options
109
+
110
+ | Option | Default | Description |
111
+ |---|---|---|
112
+ | `default` | none (fail-closed) | Baseline policy applied when no more specific rule matches. `allow` lists the permitted git operations; `deny` lists blocked ones. With no `default.allow`, every mutation is blocked. |
113
+ | `branches` | none | Policy keyed by exact branch name, resolved at command time from the VCS. A matching entry replaces the baseline `allow` and unions its `deny` with the baseline. |
114
+ | `repos` | none | Policy keyed by absolute location directory. Takes precedence over `branches` and `default`. |
115
+
116
+ A policy is `{ "allow": string[], "deny": string[] }`. Both fields are optional.
117
+
118
+ Recognized git operations: `add`, `branch`, `checkout`, `cherry-pick`, `clean`,
119
+ `commit`, `merge`, `mv`, `push`, `rebase`, `reset`, `restore`, `revert`, `rm`,
120
+ `stash`, `switch`, `tag`.
121
+
122
+ ### Semantics
123
+
124
+ - **Fail-closed.** No config, or an empty resolved `allow`, blocks every git
125
+ mutation.
126
+ - **Hierarchical.** `default` is the baseline. A `branches.<name>` entry
127
+ overrides `allow` (replaces the baseline) and `deny` (unions with the
128
+ baseline). A `repos.<directory>` entry overrides both.
129
+ - **`deny` wins.** An operation is allowed only if it is in the resolved `allow`
130
+ and not in the resolved `deny`.
131
+ - **Exact branch match.** The branch is resolved at call time with
132
+ `git branch --show-current`. No globs.
133
+ - **Read-only commands pass.** `status`, `log`, `diff`, `fetch` and anything not
134
+ in the mutation list are never blocked.
135
+
136
+ The example above gives `main`/`master` an empty `allow` (no mutations), while
137
+ every other branch inherits the permissive default.
138
+
139
+ ### Per-repository override
140
+
141
+ Allow work in a single checkout even on a protected branch by keying it on the
142
+ location directory:
143
+
144
+ ```jsonc
145
+ {
146
+ "plugins": [
147
+ {
148
+ "package": "opencode-branch-guard",
149
+ "options": {
150
+ "default": { "allow": ["commit", "push"] },
151
+ "branches": { "main": { "allow": [] } },
152
+ "repos": {
153
+ "/home/you/code/projects/scratch": {
154
+ "allow": ["add", "commit", "push", "reset", "stash", "switch"]
155
+ }
156
+ }
157
+ }
158
+ }
159
+ ]
160
+ }
161
+ ```
162
+
163
+ ### Blocklist instead of allowlist
164
+
165
+ Use `deny` when you want a permissive baseline with a few hard blocks:
166
+
167
+ ```jsonc
168
+ {
169
+ "default": { "allow": ["commit", "push", "add", "checkout", "merge"], "deny": ["push"] }
170
+ }
171
+ ```
172
+
173
+ `deny` wins over `allow`, so `push` is blocked even though it is listed.
174
+
175
+ ## Limitations
176
+
177
+ The plugin inspects the shell command string, so it can be bypassed by:
178
+
179
+ - Git hidden behind a wrapper the scanner does not unwrap (`sudo git commit`,
180
+ shell aliases, scripts that call git).
181
+ - Git invoked through a tool other than the shell tool (for example a
182
+ subprocess started by a program the agent runs).
183
+ - Compound commands the scanner cannot split.
184
+
185
+ Also note that `branch` is treated as a mutation, so `git branch` and
186
+ `git branch --show-current` are blocked on a protected branch. Use
187
+ `git rev-parse --abbrev-ref HEAD` if you need a read-only branch check there.
188
+
189
+ It is a guardrail against accidental mutations, not a security boundary.
190
+
191
+ ## Verify
192
+
193
+ After configuring, restart OpenCode and try:
194
+
195
+ 1. On `main`: ask the agent to run `git commit` — the command is denied with
196
+ `Blocked: git commit is not allowed by config`.
197
+ 2. On a feature branch: the same command is allowed.
198
+ 3. `git status` and `git log` are always allowed.
199
+ 4. On a directory listed in `repos`, the repository policy applies.
200
+
201
+ ## Uninstall
202
+
203
+ ```sh
204
+ opencode plugin remove opencode-branch-guard
205
+ ```
206
+
207
+ Or remove the entry from `plugins` in your `opencode.jsonc` and restart
208
+ OpenCode.
209
+
210
+ ## Development
211
+
212
+ ```sh
213
+ bun install
214
+ bun run typecheck # tsc --noEmit, strict
215
+ bun test # unit (core logic) + functional (mocked plugin context)
216
+ bun run build # dist/index.js + dist/index.d.ts (npm entrypoint)
217
+ ```
218
+
219
+ - `src/core.ts` — pure logic: git operation parsing and policy resolution. No
220
+ OpenCode imports. Fully unit-tested.
221
+ - `src/index.ts` — the plugin (`id: "branch-guard"`), a
222
+ `Plugin.define({ id, setup })` from `@opencode/plugin`. It registers a
223
+ `ctx.permission.hook("evaluate")`, resolves the session directory, and reads
224
+ the branch with `git branch --show-current`.
225
+ - `scripts/build.ts` — bundles `src/index.ts` to `dist/index.js` with
226
+ `@opencode/plugin` external, then emits declarations with `tsc`.
227
+
228
+ ## Pre-release checklist
229
+
230
+ ```sh
231
+ bun install
232
+ bun run typecheck
233
+ bun test
234
+ bun run build
235
+ npm pack --dry-run
236
+ ```
237
+
238
+ Inspect the pack list (`dist/`, `README.md`, `LICENSE` only). Scan for secrets
239
+ before `npm publish`.
240
+
241
+ ## License
242
+
243
+ MIT — see [LICENSE](./LICENSE). Author: Hugo Batista
244
+ (<https://github.com/hugobatista>).
package/dist/core.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export declare const OPS: readonly ["add", "branch", "checkout", "cherry-pick", "clean", "commit", "merge", "mv", "push", "rebase", "reset", "restore", "revert", "rm", "stash", "switch", "tag"];
2
+ export type GitOp = (typeof OPS)[number];
3
+ export type BranchPolicy = {
4
+ allow?: string[];
5
+ deny?: string[];
6
+ };
7
+ export type Config = {
8
+ default?: BranchPolicy;
9
+ branches?: Record<string, BranchPolicy>;
10
+ repos?: Record<string, BranchPolicy>;
11
+ };
12
+ export type ResolvedPolicy = {
13
+ allow: string[];
14
+ deny: string[];
15
+ };
16
+ export type Decision = {
17
+ allowed: boolean;
18
+ message?: string;
19
+ };
20
+ export declare function isGitOp(value: string): value is GitOp;
21
+ export declare function gitOp(cmd: string): GitOp | null;
22
+ export declare function resolvePolicy(config: Config, branch: string | undefined, directory: string | undefined): ResolvedPolicy;
23
+ export declare function decide(op: string, policy: ResolvedPolicy): Decision;
@@ -0,0 +1,3 @@
1
+ import { Plugin } from "@opencode/plugin";
2
+ declare const _default: Plugin.Plugin;
3
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { execFile } from "child_process";
4
+ import { promisify } from "util";
5
+ import { Plugin } from "@opencode/plugin";
6
+
7
+ // src/core.ts
8
+ var OPS = [
9
+ "add",
10
+ "branch",
11
+ "checkout",
12
+ "cherry-pick",
13
+ "clean",
14
+ "commit",
15
+ "merge",
16
+ "mv",
17
+ "push",
18
+ "rebase",
19
+ "reset",
20
+ "restore",
21
+ "revert",
22
+ "rm",
23
+ "stash",
24
+ "switch",
25
+ "tag"
26
+ ];
27
+ var FLAG_WITH_VALUE = new Set([
28
+ "-C",
29
+ "-c",
30
+ "--git-dir",
31
+ "--work-tree",
32
+ "--exec-path",
33
+ "--namespace",
34
+ "--separate-git-dir",
35
+ "--config-env"
36
+ ]);
37
+ function isGitOp(value) {
38
+ return OPS.includes(value);
39
+ }
40
+ function gitOp(cmd) {
41
+ const tokens = cmd.trim().split(/\s+/).filter(Boolean);
42
+ if (tokens[0] !== "git")
43
+ return null;
44
+ let i = 1;
45
+ while (i < tokens.length) {
46
+ const token = tokens[i];
47
+ if (token === undefined)
48
+ return null;
49
+ if (!token.startsWith("-")) {
50
+ return isGitOp(token) ? token : null;
51
+ }
52
+ if (token.startsWith("--") && token.includes("=")) {
53
+ i += 1;
54
+ } else if (FLAG_WITH_VALUE.has(token)) {
55
+ i += 2;
56
+ } else {
57
+ i += 1;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ function resolvePolicy(config, branch, directory) {
63
+ const base = config.default ?? {};
64
+ const branchRule = branch !== undefined ? config.branches?.[branch] : undefined;
65
+ const repoRule = directory !== undefined ? config.repos?.[directory] : undefined;
66
+ const rule = repoRule ?? branchRule;
67
+ const allow = rule?.allow ?? base.allow ?? [];
68
+ const deny = [...base.deny ?? [], ...rule?.deny ?? []];
69
+ return { allow, deny };
70
+ }
71
+ function decide(op, policy) {
72
+ if (policy.deny.includes(op)) {
73
+ return { allowed: false, message: `Blocked: git ${op} is denied by config` };
74
+ }
75
+ if (!policy.allow.includes(op)) {
76
+ return { allowed: false, message: `Blocked: git ${op} is not allowed by config` };
77
+ }
78
+ return { allowed: true };
79
+ }
80
+
81
+ // src/index.ts
82
+ var execFileAsync = promisify(execFile);
83
+ async function currentBranch(directory) {
84
+ try {
85
+ const { stdout } = await execFileAsync("git", ["-C", directory, "branch", "--show-current"], { timeout: 3000, windowsHide: true });
86
+ const branch = stdout.trim();
87
+ return branch.length > 0 ? branch : undefined;
88
+ } catch {
89
+ return;
90
+ }
91
+ }
92
+ async function sessionDirectory(ctx, sessionID) {
93
+ try {
94
+ return (await ctx.session.get({ sessionID })).location.directory;
95
+ } catch {
96
+ return;
97
+ }
98
+ }
99
+ var src_default = Plugin.define({
100
+ id: "branch-guard",
101
+ async setup(ctx) {
102
+ const config = ctx.options ?? {};
103
+ const directories = new Map;
104
+ await ctx.permission.hook("evaluate", async (event) => {
105
+ if (event.action !== "shell")
106
+ return;
107
+ let directory;
108
+ if (config.branches || config.repos) {
109
+ const cached = directories.get(event.sessionID);
110
+ if (cached !== undefined) {
111
+ directory = cached ?? undefined;
112
+ } else {
113
+ directory = await sessionDirectory(ctx, event.sessionID);
114
+ directories.set(event.sessionID, directory ?? null);
115
+ }
116
+ }
117
+ const branch = config.branches ? await currentBranch(directory ?? ctx.location.directory) : undefined;
118
+ const policy = resolvePolicy(config, branch, directory);
119
+ for (const resource of event.resources) {
120
+ const op = gitOp(resource);
121
+ if (op === null)
122
+ continue;
123
+ const decision = decide(op, policy);
124
+ if (!decision.allowed) {
125
+ event.effect = "deny";
126
+ event.message = decision.message;
127
+ return;
128
+ }
129
+ }
130
+ });
131
+ }
132
+ });
133
+ export {
134
+ src_default as default
135
+ };
136
+
137
+ //# debugId=4EB1771F16AB9A6E64756E2164756E21
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/core.ts"],
4
+ "sourcesContent": [
5
+ "import { execFile } from \"node:child_process\"\nimport { promisify } from \"node:util\"\nimport { Plugin } from \"@opencode/plugin\"\nimport { decide, gitOp, resolvePolicy, type Config } from \"./core\"\n\nconst execFileAsync = promisify(execFile)\n\n// `ctx.vcs.get()` can return a stale or empty branch, so read it from git\n// directly. Returns undefined outside a repository.\nasync function currentBranch(directory: string): Promise<string | undefined> {\n try {\n const { stdout } = await execFileAsync(\n \"git\",\n [\"-C\", directory, \"branch\", \"--show-current\"],\n { timeout: 3000, windowsHide: true },\n )\n const branch = stdout.trim()\n return branch.length > 0 ? branch : undefined\n } catch {\n return undefined\n }\n}\n\n// The plugin context's location is not necessarily the session's repository, and\n// the permission event does not carry a directory. Resolve it from the session.\nasync function sessionDirectory(ctx: Plugin.Context, sessionID: string): Promise<string | undefined> {\n try {\n return (await ctx.session.get({ sessionID })).location.directory\n } catch {\n return undefined\n }\n}\n\nexport default Plugin.define({\n id: \"branch-guard\",\n async setup(ctx) {\n const config = (ctx.options ?? {}) as Config\n const directories = new Map<string, string | null>()\n\n await ctx.permission.hook(\"evaluate\", async (event) => {\n // V2 names the shell action \"shell\"; ignore every other action.\n if (event.action !== \"shell\") return\n\n let directory: string | undefined\n if (config.branches || config.repos) {\n const cached = directories.get(event.sessionID)\n if (cached !== undefined) {\n directory = cached ?? undefined\n } else {\n directory = await sessionDirectory(ctx, event.sessionID)\n directories.set(event.sessionID, directory ?? null)\n }\n }\n\n const branch = config.branches\n ? await currentBranch(directory ?? ctx.location.directory)\n : undefined\n const policy = resolvePolicy(config, branch, directory)\n\n for (const resource of event.resources) {\n const op = gitOp(resource)\n if (op === null) continue\n const decision = decide(op, policy)\n if (!decision.allowed) {\n event.effect = \"deny\"\n event.message = decision.message\n return\n }\n }\n })\n },\n})\n",
6
+ "// Pure branch-guard logic. No OpenCode imports, so it is fully unit-testable.\n\nexport const OPS = [\n \"add\",\n \"branch\",\n \"checkout\",\n \"cherry-pick\",\n \"clean\",\n \"commit\",\n \"merge\",\n \"mv\",\n \"push\",\n \"rebase\",\n \"reset\",\n \"restore\",\n \"revert\",\n \"rm\",\n \"stash\",\n \"switch\",\n \"tag\",\n] as const\n\nexport type GitOp = (typeof OPS)[number]\n\n// Git global flags that consume the following token as their value.\nconst FLAG_WITH_VALUE = new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\",\n \"--exec-path\",\n \"--namespace\",\n \"--separate-git-dir\",\n \"--config-env\",\n])\n\nexport type BranchPolicy = {\n allow?: string[]\n deny?: string[]\n}\n\nexport type Config = {\n default?: BranchPolicy\n branches?: Record<string, BranchPolicy>\n repos?: Record<string, BranchPolicy>\n}\n\nexport type ResolvedPolicy = {\n allow: string[]\n deny: string[]\n}\n\nexport type Decision = {\n allowed: boolean\n message?: string\n}\n\nexport function isGitOp(value: string): value is GitOp {\n return (OPS as readonly string[]).includes(value)\n}\n\n// Extract the git mutation from a shell command string. Returns null for\n// non-git commands, git invocations without a known mutation, and read-only\n// commands (`status`, `log`, `diff`, ...).\nexport function gitOp(cmd: string): GitOp | null {\n const tokens = cmd.trim().split(/\\s+/).filter(Boolean)\n if (tokens[0] !== \"git\") return null\n\n let i = 1\n while (i < tokens.length) {\n const token = tokens[i]\n if (token === undefined) return null\n if (!token.startsWith(\"-\")) {\n return isGitOp(token) ? token : null\n }\n if (token.startsWith(\"--\") && token.includes(\"=\")) {\n i += 1\n } else if (FLAG_WITH_VALUE.has(token)) {\n i += 2\n } else {\n i += 1\n }\n }\n return null\n}\n\n// Resolve the effective policy for a command. `repos[directory]` overrides\n// `branches[branch]`, which overrides `default`. A rule's `allow` replaces the\n// baseline; its `deny` unions with the baseline. No resolved `allow` means no\n// mutation is permitted (fail-closed).\nexport function resolvePolicy(\n config: Config,\n branch: string | undefined,\n directory: string | undefined,\n): ResolvedPolicy {\n const base = config.default ?? {}\n const branchRule = branch !== undefined ? config.branches?.[branch] : undefined\n const repoRule = directory !== undefined ? config.repos?.[directory] : undefined\n const rule = repoRule ?? branchRule\n const allow = rule?.allow ?? base.allow ?? []\n const deny = [...(base.deny ?? []), ...(rule?.deny ?? [])]\n return { allow, deny }\n}\n\nexport function decide(op: string, policy: ResolvedPolicy): Decision {\n if (policy.deny.includes(op)) {\n return { allowed: false, message: `Blocked: git ${op} is denied by config` }\n }\n if (!policy.allow.includes(op)) {\n return { allowed: false, message: `Blocked: git ${op} is not allowed by config` }\n }\n return { allowed: true }\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "4EB1771F16AB9A6E64756E2164756E21",
10
+ "names": []
11
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "opencode-branch-guard",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin: blocks git mutations (commit, push, merge, rebase, ...) based on the current branch or repository, so protected branches stay clean.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Hugo Batista",
9
+ "url": "https://github.com/hugobatista"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/hugobatista/opencode-branch-guard.git"
14
+ },
15
+ "homepage": "https://github.com/hugobatista/opencode-branch-guard#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/hugobatista/opencode-branch-guard/issues"
18
+ },
19
+ "keywords": [
20
+ "opencode",
21
+ "opencode-plugin",
22
+ "git",
23
+ "branch",
24
+ "guard",
25
+ "permissions"
26
+ ],
27
+ "engines": {
28
+ "opencode": ">=2.0.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js",
42
+ "default": "./dist/index.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "test": "bun test",
47
+ "typecheck": "tsc --noEmit",
48
+ "build": "bun run scripts/build.ts",
49
+ "prepublishOnly": "bun run typecheck && bun test && bun run build"
50
+ },
51
+ "peerDependencies": {
52
+ "@opencode/plugin": ">=2.0.10"
53
+ },
54
+ "devDependencies": {
55
+ "@opencode/plugin": "2.0.10",
56
+ "@types/bun": "^1.4.1",
57
+ "@types/node": "^24.0.0",
58
+ "typescript": "^5.8.0"
59
+ }
60
+ }