@rehydra/opencode 0.14.0 → 0.16.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 +102 -31
- package/index.d.ts +35 -9
- package/index.js +31 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Scrub detected secrets from OpenCode messages before the main LLM request.
|
|
4
4
|
|
|
5
|
-
This plugin intercepts the conversation between [OpenCode](https://github.com/
|
|
5
|
+
This plugin intercepts the conversation between [OpenCode](https://github.com/anomalyco/opencode) and the LLM. Secrets from your `.env` files are replaced with placeholders before they leave your machine, and transparently restored before any tool (shell commands, file writes, etc.) executes locally.
|
|
6
6
|
|
|
7
|
-
Detected values are masked in requests that pass through the plugin hooks. Local tools receive the restored values.
|
|
7
|
+
Detected values are masked in requests that pass through the plugin hooks. Local tools receive the restored values.
|
|
8
|
+
|
|
9
|
+
**OpenCode compatibility:** the default entrypoint supports both OpenCode 2 (V2 plugin API) and OpenCode 1 (V2 converts the old `plugin` config key and calls `server()` for object entrypoints; OpenCode 1 releases older than 1.18.29 should import the `rehydra` function export or pin `@rehydra/opencode@0.14`). The same package ships both implementations:
|
|
10
|
+
|
|
11
|
+
- OpenCode 2 — `Plugin.define({ id: "rehydra", setup(ctx) })` via `createRehydraPluginV2()`
|
|
12
|
+
- OpenCode 1 — classic hooks object via `createRehydraPlugin()`
|
|
8
13
|
|
|
9
14
|
## Install
|
|
10
15
|
|
|
@@ -12,7 +17,15 @@ Detected values are masked in requests that pass through the plugin hooks. Local
|
|
|
12
17
|
npm install @rehydra/opencode
|
|
13
18
|
```
|
|
14
19
|
|
|
15
|
-
Add to `opencode.json
|
|
20
|
+
Add to `opencode.json` — **OpenCode 2** uses the plural `plugins` key:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"plugins": ["@rehydra/opencode"]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**OpenCode 1** uses the singular `plugin` key:
|
|
16
29
|
|
|
17
30
|
```json
|
|
18
31
|
{
|
|
@@ -22,33 +35,56 @@ Add to `opencode.json`:
|
|
|
22
35
|
|
|
23
36
|
By default, the plugin discovers `**/.env*` under the OpenCode project directory, including files such as `packages/api/.env` and `apps/web/.env.local`. It skips `node_modules`, `.git`, and symbolic links. Secrets with values of 4+ characters are detected and scrubbed.
|
|
24
37
|
|
|
25
|
-
`envFiles` accepts exact paths and glob patterns, resolved against the project directory even when OpenCode starts elsewhere. Use `envFiles: [".env"]` for root-only loading, or `envFiles: []` to disable file loading.
|
|
38
|
+
`envFiles` accepts exact paths and glob patterns, resolved against the project directory even when OpenCode starts elsewhere. Use `envFiles: [".env"]` for root-only loading, or `envFiles: []` to disable file loading. Before a model request, the plugin checks the files again if at least `envRefreshInterval` milliseconds (default 2000) have passed since the last check, so new or changed secrets are picked up without restarting OpenCode. Set `envRefreshInterval: false` to load them only at startup. Missing files are ignored. An advanced `anonymizer` configuration keeps control of its own `secrets` settings, with relative paths still rooted at the project directory unless `secrets.envBaseDirectory` is set.
|
|
26
39
|
|
|
27
|
-
##
|
|
40
|
+
## Configuration
|
|
28
41
|
|
|
29
|
-
OpenCode
|
|
42
|
+
### OpenCode 2 — plugin options
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
OpenCode 2 passes options natively, with no local plugin file needed:
|
|
32
45
|
|
|
33
46
|
```json
|
|
34
47
|
{
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
"plugins": [
|
|
49
|
+
{
|
|
50
|
+
"package": "@rehydra/opencode",
|
|
51
|
+
"options": {
|
|
52
|
+
"envFiles": [".env", ".env.local", ".env.production"],
|
|
53
|
+
"redactValues": ["sk-live-abc123..."],
|
|
54
|
+
"minValueLength": 6,
|
|
55
|
+
"disableTypes": ["URL", "IP_ADDRESS"],
|
|
56
|
+
"redactTools": ["webfetch", "websearch", "exa_*"],
|
|
57
|
+
"vcsIdentities": true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
39
61
|
}
|
|
40
62
|
```
|
|
41
63
|
|
|
42
|
-
|
|
64
|
+
Options in the config object map 1:1 to `RehydraPluginOptions`. When both config options and factory options are given, factory options win per key.
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
### OpenCode 1 — plugin options
|
|
67
|
+
|
|
68
|
+
OpenCode 1 takes options as the second element of a `[package, options]` entry:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"plugin": [
|
|
73
|
+
["@rehydra/opencode", { "disableTypes": ["URL", "IP_ADDRESS", "DATE"] }]
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`disableTypes` replaces the default list, so keep `URL` and `IP_ADDRESS` in it if they should stay disabled.
|
|
79
|
+
|
|
80
|
+
### Local plugin file
|
|
45
81
|
|
|
46
|
-
For custom settings
|
|
82
|
+
For custom settings via a local plugin file (`.opencode/plugins/rehydra.ts`), use the factory matching your OpenCode version:
|
|
47
83
|
|
|
48
84
|
```typescript
|
|
49
|
-
import {
|
|
85
|
+
import { createRehydraPluginV2 } from "@rehydra/opencode";
|
|
50
86
|
|
|
51
|
-
export default
|
|
87
|
+
export default createRehydraPluginV2({
|
|
52
88
|
// Scan multiple env files
|
|
53
89
|
envFiles: [".env", ".env.local", ".env.production"],
|
|
54
90
|
|
|
@@ -61,16 +97,19 @@ export default createRehydraPlugin({
|
|
|
61
97
|
// Disable detection of specific PII types
|
|
62
98
|
disableTypes: ["URL", "IP_ADDRESS"],
|
|
63
99
|
|
|
100
|
+
// Keep placeholders in the arguments of tools that call external services
|
|
101
|
+
redactTools: ["webfetch", "websearch", "exa_*"],
|
|
102
|
+
|
|
64
103
|
// Redact identities in Git and GitHub CLI output
|
|
65
104
|
vcsIdentities: true,
|
|
66
105
|
});
|
|
67
106
|
```
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
every occurrence of those identities in the same tool output without touching
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
On OpenCode 1, import `createRehydraPlugin` instead (it returns the callable V1 plugin).
|
|
109
|
+
|
|
110
|
+
`vcsIdentities` detects logins in `gh pr` and `gh api` output, plus author and committer names in `git log`, `git show`, and `git blame` output. It redacts every occurrence of those identities in the same tool output without touching npm scopes or other `@` identifiers in unrelated commands. GitHub display names outside these structured fields still require the optional local NER model.
|
|
111
|
+
|
|
112
|
+
`redactTools` lists tools whose arguments keep their placeholders. The plugin normally restores real values before a tool runs. For these tools it leaves the placeholders in place and redacts any other detected values in the arguments, so secrets and PII don't reach external services through tools such as `webfetch` or `websearch`. `*` matches any characters, which helps with MCP tools: `"exa_*"`. The default is `["webfetch", "websearch"]`. Setting `redactTools` replaces it, so keep those two in your list, or pass `[]` to restore real values for every tool.
|
|
74
113
|
|
|
75
114
|
## What gets detected
|
|
76
115
|
|
|
@@ -82,21 +121,53 @@ outside these structured fields still require the optional local NER model.
|
|
|
82
121
|
|
|
83
122
|
## How it works
|
|
84
123
|
|
|
85
|
-
|
|
124
|
+
### OpenCode 2 hooks
|
|
125
|
+
|
|
126
|
+
| Hook | What it does |
|
|
127
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
128
|
+
| `session.hook("context" / "compaction" / "generate" / "title")` | Scrubs secrets from all message text before each model request |
|
|
129
|
+
| `event.system.push(...)` | Tells the LLM to treat placeholders as real values |
|
|
130
|
+
| `tool.hook("execute.before")` | Restores real values in tool arguments before local execution, except for `redactTools` |
|
|
131
|
+
| `tool.hook("execute.after")` | Restores real values in displayed tool output |
|
|
132
|
+
|
|
133
|
+
The scrubber is registered on all four session-request kinds, so title generation, checkpoint compaction, and transient generates are protected too.
|
|
134
|
+
|
|
135
|
+
### OpenCode 1 hooks
|
|
136
|
+
|
|
137
|
+
| Hook | What it does |
|
|
138
|
+
| -------------------------------------- | -------------------------------------------------------------- |
|
|
139
|
+
| `experimental.chat.messages.transform` | Scrubs secrets from all message text before it reaches the LLM |
|
|
140
|
+
| `experimental.chat.system.transform` | Tells the LLM to treat placeholders as real values |
|
|
141
|
+
| `tool.execute.before` | Restores real values in tool arguments before local execution, except for `redactTools` |
|
|
142
|
+
| `tool.execute.after` | Restores real values in displayed tool output |
|
|
143
|
+
| `experimental.text.complete` | Restores real values in LLM response text shown to you |
|
|
144
|
+
|
|
145
|
+
Detection and rehydration run locally. No data leaves your machine except the scrubbed conversation sent to the LLM provider.
|
|
146
|
+
|
|
147
|
+
## Known limitations
|
|
148
|
+
|
|
149
|
+
### Session titles — fixed on OpenCode 2
|
|
150
|
+
|
|
151
|
+
OpenCode 1 sends the first user message to a separate LLM call to generate a session title. That call bypasses `experimental.chat.messages.transform`, so the V1 plugin cannot scrub it ([OpenCode issue #46115](https://github.com/anomalyco/opencode/issues/46115)). On OpenCode 1, disable the title agent:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"plugin": ["@rehydra/opencode"],
|
|
156
|
+
"agent": {
|
|
157
|
+
"title": { "disable": true }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
On OpenCode 2 this workaround is unnecessary — the plugin registers a `title` session hook and scrubs the request.
|
|
86
163
|
|
|
87
|
-
|
|
88
|
-
|---|---|
|
|
89
|
-
| `messages.transform` | Scrubs secrets from all message text before it reaches the LLM |
|
|
90
|
-
| `system.transform` | Tells the LLM to treat placeholders as real values |
|
|
91
|
-
| `tool.execute.before` | Restores real values in tool arguments before local execution |
|
|
92
|
-
| `tool.execute.after` | Restores real values in displayed tool output |
|
|
93
|
-
| `text.complete` | Restores real values in LLM response text shown to you |
|
|
164
|
+
### Final response text on OpenCode 2
|
|
94
165
|
|
|
95
|
-
|
|
166
|
+
OpenCode 2 removed the `experimental.text.complete` hook with no replacement. Tool execution and outgoing conversation history stay protected; assistant responses that echo a PII placeholder into chat text will display the placeholder (`<PII type="…" id="…"/>`) instead of the real value. Tool arguments referencing the placeholder still receive real values before local execution.
|
|
96
167
|
|
|
97
168
|
## Logging
|
|
98
169
|
|
|
99
|
-
Plugin activity is logged to OpenCode's log directory (`~/.local/share/opencode/log/`). Run with `--log-level DEBUG` for detailed output.
|
|
170
|
+
Plugin activity is logged to OpenCode's log directory (`~/.local/share/opencode/log/`). OpenCode 2 plugins log through the console (captured by OpenCode's log service); OpenCode 1 plugins log through the internal log API. Run with `--log-level DEBUG` for detailed output.
|
|
100
171
|
|
|
101
172
|
```
|
|
102
173
|
INFO service=rehydra scrubbed={"ENV_VAR_SECRET":2} messageCount=3 scrubbed 2 secret(s) from messages
|
package/index.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import type { RehydraPluginOptions } from "rehydra/opencode-plugin";
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
RehydraPluginOptions,
|
|
5
|
+
RehydraV2Plugin,
|
|
6
|
+
} from "rehydra/opencode-plugin";
|
|
7
|
+
export {
|
|
8
|
+
createRehydraPlugin,
|
|
9
|
+
createRehydraPluginV2,
|
|
10
|
+
} from "rehydra/opencode-plugin";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* OpenCode 1 plugin function (named export).
|
|
14
|
+
* OpenCode 1 calls named exports as plugin functions.
|
|
15
|
+
*/
|
|
16
|
+
export declare const rehydra: (
|
|
17
|
+
input: {
|
|
18
|
+
client: unknown;
|
|
19
|
+
directory: string;
|
|
20
|
+
worktree: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
},
|
|
23
|
+
options?: RehydraPluginOptions,
|
|
24
|
+
) => Promise<Record<string, unknown>>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Dual V1/V2 default entrypoint.
|
|
28
|
+
* OpenCode 2 reads `id` + `setup()`; OpenCode 1 (>= 1.18.29) calls `server()`.
|
|
29
|
+
*/
|
|
30
|
+
declare const _default: {
|
|
31
|
+
id: string;
|
|
32
|
+
setup: (ctx: unknown) => Promise<void | (() => void)>;
|
|
33
|
+
server: (input?: unknown, options?: RehydraPluginOptions) => Promise<Record<string, unknown>>;
|
|
34
|
+
};
|
|
35
|
+
export default _default;
|
package/index.js
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createRehydraPlugin,
|
|
3
|
+
createRehydraPluginV2,
|
|
4
|
+
} from "rehydra/opencode-plugin";
|
|
2
5
|
|
|
3
|
-
// Named
|
|
4
|
-
|
|
6
|
+
// Named factories for custom config:
|
|
7
|
+
// - createRehydraPlugin — OpenCode 1 (returns the callable V1 plugin)
|
|
8
|
+
// - createRehydraPluginV2 — OpenCode 2 (returns the { id, setup } definition)
|
|
9
|
+
// For OpenCode 2 with custom options prefer the config "options" form:
|
|
10
|
+
// { "plugins": [{ "package": "@rehydra/opencode", "options": { ... } }] }
|
|
11
|
+
export { createRehydraPlugin, createRehydraPluginV2 };
|
|
5
12
|
|
|
6
|
-
// Named
|
|
7
|
-
//
|
|
8
|
-
// the Plugin itself (takes ctx, returns hooks), not the factory.
|
|
13
|
+
// Named export for OpenCode 1: OpenCode calls named exports as plugin
|
|
14
|
+
// functions, so this must be the V1 Plugin itself (takes ctx, returns hooks).
|
|
9
15
|
export const rehydra = createRehydraPlugin();
|
|
10
|
-
|
|
16
|
+
|
|
17
|
+
// Default export: dual V1/V2 entrypoint.
|
|
18
|
+
// - OpenCode 2 reads `id` and `setup()`, ignoring `server()`.
|
|
19
|
+
// - OpenCode 1 (>= 1.18.29 object entrypoints) calls `server()` and uses the
|
|
20
|
+
// returned hooks. V1 releases older than 1.18.29 should import `rehydra`.
|
|
21
|
+
const v2 = createRehydraPluginV2();
|
|
22
|
+
export default {
|
|
23
|
+
id: v2.id,
|
|
24
|
+
setup: v2.setup,
|
|
25
|
+
async server(input, options) {
|
|
26
|
+
const plugin = createRehydraPlugin();
|
|
27
|
+
const resolved = input ?? {
|
|
28
|
+
client: { app: { log: () => Promise.resolve(undefined) } },
|
|
29
|
+
directory: process.cwd(),
|
|
30
|
+
worktree: process.cwd(),
|
|
31
|
+
};
|
|
32
|
+
return await plugin(resolved, options);
|
|
33
|
+
},
|
|
34
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehydra/opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Rehydra plugin for OpenCode — anonymizes secrets in LLM context using native plugin hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"directory": "packages/opencode-plugin"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"rehydra": "^0.
|
|
32
|
+
"rehydra": "^0.16.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=18.0.0"
|