@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.
Files changed (4) hide show
  1. package/README.md +102 -31
  2. package/index.d.ts +35 -9
  3. package/index.js +31 -7
  4. 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/sst/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.
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. See the title-generation limitation below.
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. Files are loaded once when the plugin initializes; restart OpenCode after changing them. 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.
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
- ## Session title limitation
40
+ ## Configuration
28
41
 
29
- OpenCode also sends the first user message to a separate LLM call to generate a session title. That call bypasses `experimental.chat.messages.transform`, so the plugin cannot scrub it. The first message can reach the title model with secrets intact even when the main conversation is anonymized. This limitation is tracked in [OpenCode issue #46115](https://github.com/anomalyco/opencode/issues/46115).
42
+ ### OpenCode 2 — plugin options
30
43
 
31
- To prevent that request, disable OpenCode's title agent in `opencode.json`:
44
+ OpenCode 2 passes options natively, with no local plugin file needed:
32
45
 
33
46
  ```json
34
47
  {
35
- "plugin": ["@rehydra/opencode"],
36
- "agent": {
37
- "title": { "disable": true }
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
- This turns off automatic session titles. Restart OpenCode after changing the configuration. The plugin protects requests that invoke its hooks; it cannot intercept other model calls made outside those hooks.
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
- ## Configuration
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, create `.opencode/plugins/rehydra.ts`:
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 { createRehydraPlugin } from "@rehydra/opencode";
85
+ import { createRehydraPluginV2 } from "@rehydra/opencode";
50
86
 
51
- export default createRehydraPlugin({
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
- `vcsIdentities` detects logins in `gh pr` and `gh api` output, plus author and
70
- committer names in `git log`, `git show`, and `git blame` output. It redacts
71
- every occurrence of those identities in the same tool output without touching
72
- npm scopes or other `@` identifiers in unrelated commands. GitHub display names
73
- outside these structured fields still require the optional local NER model.
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
- The plugin uses five OpenCode hooks:
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
- | Hook | What it does |
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
- Detection and rehydration run locally. The main conversation is scrubbed through the message hook before forwarding to the LLM provider. The separate title request described above needs its own opt-out.
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
- export { createRehydraPlugin } from "rehydra/opencode-plugin";
2
- export type { RehydraPluginOptions } from "rehydra/opencode-plugin";
3
- export declare const rehydra: (input: {
4
- client: unknown;
5
- directory: string;
6
- worktree: string;
7
- [key: string]: unknown;
8
- }) => Promise<Record<string, unknown>>;
9
- export default rehydra;
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 { createRehydraPlugin } from "rehydra/opencode-plugin";
1
+ import {
2
+ createRehydraPlugin,
3
+ createRehydraPluginV2,
4
+ } from "rehydra/opencode-plugin";
2
5
 
3
- // Named export: the factory for custom config (.opencode/plugins/rehydra.ts)
4
- export { createRehydraPlugin };
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 + default export: the plugin function for opencode.json usage.
7
- // OpenCode calls named exports as plugin functions, so this must be
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
- export default rehydra;
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.14.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.14.0"
32
+ "rehydra": "^0.16.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=18.0.0"