opencode-varlock 0.0.1

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 itlackey
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,352 @@
1
+ # opencode-varlock
2
+
3
+ OpenCode plugin that gives agents access to environment variables **without revealing secret values**.
4
+
5
+ ## The Problem
6
+
7
+ When an AI agent needs secrets (database URLs, API keys, tokens) to run your code, the obvious approach — letting it read `.env` — puts every secret directly into its context window. It can then echo them, log them, or hallucinate them into committed code.
8
+
9
+ ## How It Works
10
+
11
+ Three layers enforce the boundary:
12
+
13
+ ```
14
+ ┌──────────────────────────────────────────────────┐
15
+ │ Agent context window │
16
+ │ │
17
+ │ "Loaded 3 vars: DB_URL, API_KEY, REDIS_HOST" │
18
+ │ ↑ names only, never values │
19
+ │ │
20
+ ├──────────────────────────────────────────────────┤
21
+ │ Layer 1 — Custom tools (load_env / load_secrets)│
22
+ │ Reads files or calls Varlock CLI, injects into │
23
+ │ process.env, returns only key names. │
24
+ ├──────────────────────────────────────────────────┤
25
+ │ Layer 2 — Permission rules │
26
+ │ Glob-based deny rules block `cat .env`, │
27
+ │ `printenv`, `echo $SECRET`, etc. │
28
+ ├──────────────────────────────────────────────────┤
29
+ │ Layer 3 — EnvGuard hook │
30
+ │ tool.execute.before intercept catches anything │
31
+ │ the glob rules miss (python -c, scripting │
32
+ │ escapes, indirect reads). │
33
+ └──────────────────────────────────────────────────┘
34
+ ```
35
+
36
+ ## Install
37
+
38
+ ### As an npm plugin
39
+
40
+ ```bash
41
+ npm install opencode-varlock
42
+ ```
43
+
44
+ ```json
45
+ // opencode.json
46
+ {
47
+ "plugin": ["opencode-varlock"]
48
+ }
49
+ ```
50
+
51
+ The published package ships compiled ESM in `dist/`, and the root entry exports only the plugin itself so OpenCode can load it cleanly through the normal npm plugin resolution flow.
52
+
53
+ ### As a local plugin
54
+
55
+ Reference the package directory locally after building it:
56
+
57
+ ```json
58
+ {
59
+ "plugin": ["./path/to/opencode-varlock"]
60
+ }
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ All configuration lives in a single `varlock.config.json` file. The plugin searches for it in two locations (merged in order):
66
+
67
+ 1. `./varlock.config.json` (project root)
68
+ 2. `.opencode/varlock.config.json`
69
+
70
+ Programmatic overrides passed to `createVarlockPlugin()` take highest priority.
71
+
72
+ ### Quick start
73
+
74
+ Copy the default config into your project:
75
+
76
+ ```bash
77
+ cp node_modules/opencode-varlock/assets/varlock.config.json ./varlock.config.json
78
+ ```
79
+
80
+ Or create a minimal one — only the fields you want to change:
81
+
82
+ ```json
83
+ {
84
+ "varlock": {
85
+ "enabled": true,
86
+ "namespace": "myapp"
87
+ }
88
+ }
89
+ ```
90
+
91
+ Everything else inherits from the built-in defaults.
92
+
93
+ The bundled template and permission presets now live in `assets/`:
94
+
95
+ ```text
96
+ assets/varlock.config.json
97
+ assets/varlock.schema.json
98
+ assets/permissions.json
99
+ ```
100
+
101
+ The copied config template points its `$schema` at `./node_modules/opencode-varlock/assets/varlock.schema.json`, so editors can validate and autocomplete it after install.
102
+
103
+ ## Repo layout
104
+
105
+ ```text
106
+ src/ TypeScript source for the plugin entry, config, guard, and tools
107
+ assets/ JSON assets shipped with the npm package
108
+ docs/ Setup and integration guides
109
+ ```
110
+
111
+ ### Full config reference
112
+
113
+ ```json
114
+ {
115
+ "guard": {
116
+ "enabled": true,
117
+
118
+ "sensitivePatterns": [
119
+ ".env", ".secret", ".pem", ".key", "credentials", ".pgpass"
120
+ ],
121
+
122
+ "sensitiveGlobs": [
123
+ "**/.env",
124
+ "**/.env.*",
125
+ "**/.env.local",
126
+ "**/.env.production",
127
+ "**/*.pem",
128
+ "**/*.key",
129
+ "**/credentials",
130
+ "**/credentials.*",
131
+ "**/.pgpass",
132
+ "secrets/**"
133
+ ],
134
+
135
+ "bashDenyPatterns": [],
136
+
137
+ "blockedReadTools": ["read", "grep", "glob", "view"],
138
+ "blockedWriteTools": ["write", "edit"]
139
+ },
140
+
141
+ "env": {
142
+ "enabled": true,
143
+ "allowedRoot": "."
144
+ },
145
+
146
+ "varlock": {
147
+ "enabled": false,
148
+ "autoDetect": true,
149
+ "command": "varlock",
150
+ "namespace": "app"
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### Config sections
156
+
157
+ #### `guard` — EnvGuard hook
158
+
159
+ | Field | Type | Default | Description |
160
+ |---|---|---|---|
161
+ | `enabled` | `boolean` | `true` | Master switch for the `tool.execute.before` hook |
162
+ | `sensitivePatterns` | `string[]` | see above | Substring patterns — a path containing any of these is blocked |
163
+ | `sensitiveGlobs` | `string[]` | see above | Glob patterns — matched against full paths using `*`, `**`, `?` |
164
+ | `bashDenyPatterns` | `string[]` | `[]` | Extra bash substrings to deny (merged with ~30 built-ins) |
165
+ | `blockedReadTools` | `string[]` | `["read","grep","glob","view"]` | Tool names that trigger the file-read check |
166
+ | `blockedWriteTools` | `string[]` | `["write","edit"]` | Tool names that trigger the file-write check |
167
+
168
+ #### `env` — .env file loader
169
+
170
+ | Field | Type | Default | Description |
171
+ |---|---|---|---|
172
+ | `enabled` | `boolean` | `true` | Register the `load_env` tool |
173
+ | `allowedRoot` | `string` | `"."` | Path containment boundary (resolved relative to cwd) |
174
+
175
+ #### `varlock` — Varlock integration
176
+
177
+ | Field | Type | Default | Description |
178
+ |---|---|---|---|
179
+ | `enabled` | `boolean` | `false` | Explicitly enable Varlock tools |
180
+ | `autoDetect` | `boolean` | `true` | Probe for the CLI at startup and enable if found |
181
+ | `command` | `string` | `"varlock"` | Path or name of the Varlock binary |
182
+ | `namespace` | `string` | `"app"` | Default namespace for `load_secrets` / `secret_status` |
183
+
184
+ **Varlock resolution logic:**
185
+
186
+ - `enabled: true` → tools are registered (fails at runtime if CLI is missing)
187
+ - `enabled: false, autoDetect: true` → probes `which <command>`, enables if found
188
+ - `enabled: false, autoDetect: false` → Varlock is fully disabled
189
+
190
+ ### Config merge behavior
191
+
192
+ Arrays are **replaced**, not concatenated. This means you can fully override the default glob list in your config file without inheriting the defaults:
193
+
194
+ ```json
195
+ {
196
+ "guard": {
197
+ "sensitiveGlobs": ["secrets/**", "config/.env.*"]
198
+ }
199
+ }
200
+ ```
201
+
202
+ Object sections are deep-merged. Scalar values overwrite.
203
+
204
+ ### Programmatic overrides
205
+
206
+ For project-specific customization beyond what JSON can express:
207
+
208
+ ```typescript
209
+ // .opencode/plugin/secrets.ts
210
+ import { createVarlockPlugin } from "opencode-varlock/plugin"
211
+
212
+ export default createVarlockPlugin({
213
+ guard: {
214
+ sensitiveGlobs: [
215
+ "**/.env",
216
+ "**/.env.*",
217
+ "infra/secrets/**",
218
+ "deploy/*.key",
219
+ ],
220
+ bashDenyPatterns: ["vault read", "aws secretsmanager"],
221
+ },
222
+ varlock: {
223
+ enabled: true,
224
+ command: "/usr/local/bin/varlock",
225
+ namespace: "prod",
226
+ },
227
+ })
228
+ ```
229
+
230
+ ## Glob patterns
231
+
232
+ The guard supports glob patterns alongside the existing substring patterns. Both are checked — a match on either blocks the access.
233
+
234
+ ### Supported syntax
235
+
236
+ | Pattern | Matches | Example |
237
+ |---|---|---|
238
+ | `*` | Any characters except `/` | `*.pem` matches `server.pem` |
239
+ | `**` | Any characters including `/` | `secrets/**` matches `secrets/prod/db.key` |
240
+ | `**/` | Zero or more directory levels | `**/.env` matches `.env` and `config/.env` |
241
+ | `?` | Single character except `/` | `?.key` matches `a.key` |
242
+
243
+ ### When to use which
244
+
245
+ **Substring patterns** are fast and filename-oriented. Use them for extensions and file names that should be blocked everywhere regardless of path:
246
+
247
+ ```json
248
+ "sensitivePatterns": [".env", ".pem", "credentials"]
249
+ ```
250
+
251
+ **Glob patterns** are structural and path-aware. Use them for directory-scoped rules and more precise matching:
252
+
253
+ ```json
254
+ "sensitiveGlobs": [
255
+ "secrets/**",
256
+ "config/.env.*",
257
+ "deploy/**/*.key",
258
+ "**/node_modules/**/.env"
259
+ ]
260
+ ```
261
+
262
+ ### How globs are checked
263
+
264
+ For file tool calls (`read`, `write`, `edit`, etc.), the glob is matched against the path argument directly.
265
+
266
+ For bash commands, the guard extracts file-path-like tokens from the command string and checks each one against the compiled globs. This catches things like `jq . secrets/config.json` even when the substring patterns wouldn't flag it.
267
+
268
+ ## Tools
269
+
270
+ ### `load_env`
271
+
272
+ Parses a `.env` file and sets `process.env`. Returns only variable **names**.
273
+
274
+ ```
275
+ Agent → load_env(path: ".env")
276
+ ← { loaded: ["DATABASE_URL", "REDIS_HOST"], skipped: ["NODE_ENV"] }
277
+ ```
278
+
279
+ ### `load_secrets` (Varlock)
280
+
281
+ Pulls secrets from Varlock and injects into `process.env`.
282
+
283
+ ```
284
+ Agent → load_secrets(namespace: "prod", keys: ["db_url", "api_key"])
285
+ ← { loaded: ["DB_URL", "API_KEY"], source: "varlock/prod" }
286
+ ```
287
+
288
+ ### `secret_status` (Varlock)
289
+
290
+ Read-only check of which secrets exist and which are loaded.
291
+
292
+ ```
293
+ Agent → secret_status(namespace: "app")
294
+ ← { total: 5, loaded: 3, unloaded: 2, keys: [...] }
295
+ ```
296
+
297
+ ## Permission sets
298
+
299
+ The `assets/permissions.json` file contains three tiers (standard, strict, lockdown) plus an example agent definition. Copy the tier that fits your threat model into `opencode.json`.
300
+
301
+ These permission rules complement the EnvGuard hook — the rules handle fast-path blocking while the hook catches edge cases the glob-based rules miss.
302
+
303
+ ## Architecture
304
+
305
+ ### Why three layers?
306
+
307
+ **Permissions alone aren't enough.** An agent can try `python3 -c "print(open('.env').read())"` — the glob `cat *.env*` won't catch it.
308
+
309
+ **Prompt instructions alone aren't enough.** Telling an agent "never read .env" is a soft boundary the model can reason past.
310
+
311
+ **The plugin hook is the hard boundary.** `tool.execute.before` runs before every built-in tool call, inspects actual arguments, and throws an error the agent cannot suppress. The error message redirects it to the approved tools.
312
+
313
+ ### What the agent sees
314
+
315
+ ```
316
+ ✓ "Loaded 5 variables: DATABASE_URL, API_KEY, REDIS_HOST, JWT_SECRET, SMTP_PASS"
317
+ ✓ Writes code: const db = new Client(process.env.DATABASE_URL)
318
+ ✗ cat .env → Blocked: deny pattern
319
+ ✗ echo $API_KEY → Blocked: deny pattern
320
+ ✗ python -c "open..." → Blocked: sensitive file
321
+ ✗ jq . secrets/app.json → Blocked: matches glob "secrets/**"
322
+ ```
323
+
324
+ ## Advanced: composing individual pieces
325
+
326
+ Every component is exported for use in custom plugins:
327
+
328
+ ```typescript
329
+ import { loadConfig, DEFAULT_CONFIG } from "opencode-varlock/config"
330
+ import { createVarlockPlugin } from "opencode-varlock/plugin"
331
+ import { createEnvGuard, globToRegex } from "opencode-varlock/guard"
332
+ import { createLoadEnvTool } from "opencode-varlock/tools"
333
+
334
+ // Load config with custom overrides
335
+ const config = loadConfig(process.cwd(), {
336
+ guard: { sensitiveGlobs: ["my-secrets/**"] },
337
+ })
338
+
339
+ // Use just the guard
340
+ const guard = createEnvGuard(config.guard)
341
+
342
+ // Use just the tool
343
+ const loadEnv = createLoadEnvTool(config.env)
344
+
345
+ // Test a glob pattern
346
+ const regex = globToRegex("**/.env.*")
347
+ regex.test("config/.env.production") // true
348
+ ```
349
+
350
+ ## License
351
+
352
+ MIT
@@ -0,0 +1,156 @@
1
+ {
2
+ "$comment": [
3
+ "Recommended permission sets for use with opencode-varlock.",
4
+ "Copy the tier that matches your threat model into your opencode.json.",
5
+ "All tiers assume the VarlockPlugin is active - the plugin's EnvGuard",
6
+ "hook provides the safety net beneath these permission rules."
7
+ ],
8
+
9
+ "standard": {
10
+ "$comment": "Good default for solo devs. Blocks obvious env reads, asks for everything else.",
11
+ "permission": {
12
+ "read": "ask",
13
+ "edit": "ask",
14
+ "bash": {
15
+ "cat *.env*": "deny",
16
+ "less *.env*": "deny",
17
+ "more *.env*": "deny",
18
+ "head *.env*": "deny",
19
+ "tail *.env*": "deny",
20
+ "grep * .env*": "deny",
21
+ "echo $*": "deny",
22
+ "printenv*": "deny",
23
+ "env": "deny",
24
+ "export -p": "deny",
25
+ "source .env*": "deny",
26
+ "npm test": "allow",
27
+ "npm run *": "allow",
28
+ "bun test": "allow",
29
+ "bun run *": "allow",
30
+ "git *": "allow",
31
+ "ls *": "allow",
32
+ "*": "ask"
33
+ }
34
+ }
35
+ },
36
+
37
+ "strict": {
38
+ "$comment": "For agents operating in shared or CI environments. Denies all secret-adjacent commands, asks for bash.",
39
+ "permission": {
40
+ "read": {
41
+ "*.env*": "deny",
42
+ "*.pem": "deny",
43
+ "*.key": "deny",
44
+ "*credentials*": "deny",
45
+ "*.pgpass": "deny",
46
+ "*": "ask"
47
+ },
48
+ "write": {
49
+ "*.env*": "deny",
50
+ "*.pem": "deny",
51
+ "*.key": "deny",
52
+ "*": "ask"
53
+ },
54
+ "edit": {
55
+ "*.env*": "deny",
56
+ "*.pem": "deny",
57
+ "*.key": "deny",
58
+ "*": "ask"
59
+ },
60
+ "bash": {
61
+ "cat *.env*": "deny",
62
+ "less *.env*": "deny",
63
+ "more *.env*": "deny",
64
+ "head *.env*": "deny",
65
+ "tail *.env*": "deny",
66
+ "grep * .env*": "deny",
67
+ "echo $*": "deny",
68
+ "printenv*": "deny",
69
+ "env": "deny",
70
+ "env *": "deny",
71
+ "export -p": "deny",
72
+ "declare -x": "deny",
73
+ "source .env*": "deny",
74
+ ". .env*": "deny",
75
+ "set -a*": "deny",
76
+ "curl *env*": "deny",
77
+ "wget *env*": "deny",
78
+ "npm test": "allow",
79
+ "npm run *": "allow",
80
+ "bun test": "allow",
81
+ "bun run *": "allow",
82
+ "git *": "allow",
83
+ "ls *": "allow",
84
+ "*": "ask"
85
+ }
86
+ }
87
+ },
88
+
89
+ "lockdown": {
90
+ "$comment": "Maximum restriction. Agent can only read code and use provided tools. No bash.",
91
+ "permission": {
92
+ "read": {
93
+ "*.env*": "deny",
94
+ "*.pem": "deny",
95
+ "*.key": "deny",
96
+ "*credentials*": "deny",
97
+ "*secret*": "deny",
98
+ "*": "allow"
99
+ },
100
+ "write": {
101
+ "*.env*": "deny",
102
+ "*.ts": "ask",
103
+ "*.js": "ask",
104
+ "*": "deny"
105
+ },
106
+ "edit": {
107
+ "*.env*": "deny",
108
+ "*.ts": "ask",
109
+ "*.js": "ask",
110
+ "*": "deny"
111
+ },
112
+ "bash": "deny",
113
+ "skill": "deny"
114
+ }
115
+ },
116
+
117
+ "agent_example": {
118
+ "$comment": "Example agent definition that uses the strict tier with load_env/load_secrets tools enabled.",
119
+ "agent": {
120
+ "deploy": {
121
+ "description": "Deployment agent with managed secret access",
122
+ "mode": "subagent",
123
+ "model": "anthropic/claude-sonnet-4-20250514",
124
+ "temperature": 0.1,
125
+ "tools": {
126
+ "load_env": true,
127
+ "load_secrets": true,
128
+ "secret_status": true,
129
+ "read": true,
130
+ "grep": true,
131
+ "write": true,
132
+ "edit": true,
133
+ "bash": true
134
+ },
135
+ "permission": {
136
+ "read": {
137
+ "*.env*": "deny",
138
+ "*.pem": "deny",
139
+ "*": "allow"
140
+ },
141
+ "bash": {
142
+ "cat *.env*": "deny",
143
+ "printenv*": "deny",
144
+ "echo $*": "deny",
145
+ "env": "deny",
146
+ "docker *": "allow",
147
+ "npm *": "allow",
148
+ "bun *": "allow",
149
+ "git *": "allow",
150
+ "*": "ask"
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "./node_modules/opencode-varlock/assets/varlock.schema.json",
3
+ "$comment": "opencode-varlock configuration. Place in project root or .opencode/",
4
+
5
+ "guard": {
6
+ "$comment": "EnvGuard - the tool.execute.before hook that blocks secret exfiltration",
7
+
8
+ "enabled": true,
9
+
10
+ "sensitivePatterns": [
11
+ ".env",
12
+ ".secret",
13
+ ".pem",
14
+ ".key",
15
+ "credentials",
16
+ ".pgpass"
17
+ ],
18
+
19
+ "sensitiveGlobs": [
20
+ "**/.env",
21
+ "**/.env.*",
22
+ "**/.env.local",
23
+ "**/.env.production",
24
+ "**/*.pem",
25
+ "**/*.key",
26
+ "**/credentials",
27
+ "**/credentials.*",
28
+ "**/.pgpass",
29
+ "secrets/**"
30
+ ],
31
+
32
+ "bashDenyPatterns": [],
33
+
34
+ "blockedReadTools": ["read", "grep", "glob", "view"],
35
+ "blockedWriteTools": ["write", "edit"]
36
+ },
37
+
38
+ "env": {
39
+ "$comment": "load_env tool - parses .env files into process.env",
40
+
41
+ "enabled": true,
42
+ "allowedRoot": "."
43
+ },
44
+
45
+ "varlock": {
46
+ "$comment": "Varlock integration - pull secrets from pass, Azure KV, AWS, 1Password, etc.",
47
+
48
+ "enabled": false,
49
+ "autoDetect": true,
50
+ "command": "varlock",
51
+ "namespace": "app"
52
+ }
53
+ }
@@ -0,0 +1,105 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/itlackey/opencode-varlock/assets/varlock.schema.json",
4
+ "title": "opencode-varlock config",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string"
10
+ },
11
+ "$comment": {
12
+ "type": ["string", "array"],
13
+ "items": {
14
+ "type": "string"
15
+ }
16
+ },
17
+ "guard": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "$comment": {
22
+ "type": ["string", "array"],
23
+ "items": {
24
+ "type": "string"
25
+ }
26
+ },
27
+ "enabled": {
28
+ "type": "boolean"
29
+ },
30
+ "sensitivePatterns": {
31
+ "type": "array",
32
+ "items": {
33
+ "type": "string"
34
+ }
35
+ },
36
+ "sensitiveGlobs": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "string"
40
+ }
41
+ },
42
+ "bashDenyPatterns": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "string"
46
+ }
47
+ },
48
+ "blockedReadTools": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "blockedWriteTools": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "string"
58
+ }
59
+ }
60
+ }
61
+ },
62
+ "env": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "$comment": {
67
+ "type": ["string", "array"],
68
+ "items": {
69
+ "type": "string"
70
+ }
71
+ },
72
+ "enabled": {
73
+ "type": "boolean"
74
+ },
75
+ "allowedRoot": {
76
+ "type": "string"
77
+ }
78
+ }
79
+ },
80
+ "varlock": {
81
+ "type": "object",
82
+ "additionalProperties": false,
83
+ "properties": {
84
+ "$comment": {
85
+ "type": ["string", "array"],
86
+ "items": {
87
+ "type": "string"
88
+ }
89
+ },
90
+ "enabled": {
91
+ "type": "boolean"
92
+ },
93
+ "autoDetect": {
94
+ "type": "boolean"
95
+ },
96
+ "command": {
97
+ "type": "string"
98
+ },
99
+ "namespace": {
100
+ "type": "string"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }