Skill: parkertoddbrooks/wip-1password (version 0.2.2)

--- name: wip-1password description: AI plugin (Claude Code, OpenClaw) for 1Password secrets via JS SDK. Resolves op:// references at startup, provides agent tools for reading/writing secrets, fully headless via service account. license: MIT interface: [module, mcp, openclaw-plugin, skill] metadata: display-name: "1Password Secrets" version: "0.2.2".2.1" homepage: "https://github.com/wipcomputer/wip-1password" author: "Parker Todd Brooks" category: security capabilities: - resolve-op-refs - read-secrets - write-secrets - list-vault-items requires: bins: [node, npm] services: [1password-service-account] openclaw: requires: bins: [node, npm] plugins: [op-secrets] install: - id: node kind: node package: "@wipcomputer/wip-1password" label: "Install via npm" emoji: "🔐" compatibility: Requires Node.js 18+, 1Password with service account support (all plans). OpenClaw 2026.1+. --- # wip-1password AI plugin (Claude Code, OpenClaw) for 1Password secrets. Uses the official JavaScript SDK with service accounts for fully headless operation... no desktop app, no biometrics, no popups. ## When to Use This Skill **Use wip-1password for:** - Resolving `op://` secret references in config files at startup - Reading API keys, tokens, and credentials from 1Password at runtime - Storing new secrets in 1Password vaults - Listing available secrets in a vault ### Do NOT Use For - Managing 1Password accounts or users (use the 1Password admin console) - Accessing built-in vaults (Shared, Employee, Private). Service accounts only work with custom vaults. - Repos that don't use OpenClaw (use the `op` CLI directly instead) ## API Reference ### OpenClaw Plugin (startup resolver) Resolves `op://vault/item/field` strings in `openclaw.json` at boot. Values exist in memory only... never written to disk. ```jsonc // In openclaw.json "someService": { "apiKey": "op://Agent Secrets/Some Service/api key" } ``` ### Agent Tools ``` op_read_secret({ item: "OpenAI API", vault: "Agent Secrets", field: "api key" }) op_list_items({ vault: "Agent Secrets" }) op_write_secret({ item: "New Key", value: "sk-...", vault: "Agent Secrets" }) ``` ### Module ```typescript import { resolveSecretRefs } from "@wipcomputer/wip-1password"; const resolved = await resolveSecretRefs({ [REDACTED_SECRET] Secrets/OpenAI API/api key", }); ``` ## Setup ```bash # 1. Save service account token mkdir -p ~/.openclaw/secrets echo "ops_..." > ~/.openclaw/secrets/op-sa-token chmod 600 ~/.openclaw/secrets/op-sa-token # 2. Enable in openclaw.json # plugins.entries.op-secrets.enabled = true # plugins.entries.op-secrets.config.defaultVault = "Agent Secrets" # 3. Test openclaw op-secrets test ``` ## Key Rules - **Never call `op` bare.** Always prefix with `OP_SERVICE_ACCOUNT_TOKEN`. - **Never log secret values.** Store in variables only. - **Leave `memorySearch.remote` as `{}`** ... the plugin sets `process.env.OPENAI_API_KEY` instead. - **Service account token:** Always at `~/.openclaw/secrets/op-sa-token`. - **Custom vaults only.** Service accounts can't access built-in vaults.

path: CHANGELOG.md
content: # Changelog



## 0.2.2 (2026-03-12)

## v0.2.2: AI-Native Homepage, MCP Fix, Plan Requirements

Ran wip-1password through the WIP DevOps toolbox and rebuilt the homepage from the ground up. The README is now an AI-compatible product page. All technical documentation lives in TECHNICAL.md. The MCP server that was crashing since v0.2.0 is fixed. 1Password plan requirements updated after direct conversations with 1Password support.

---

### README rebuilt as a product page

**The problem it solved:** The old README was 400+ lines of technical documentation. Quick Start, Agent Tools API, Config Resolution, CLI Commands, Write Support, Security, Troubleshooting, Developer Guide... all in one file. No human would read it. No agent could parse it efficiently.

**What changed:** The README is now a product page. One description, a "Teach Your AI" prompt block (Karpathy pattern), four human-readable bullets, and links to docs. That's it.

The "Teach Your AI" block is the key feature. You paste it into Claude Code, ChatGPT, or any MCP-compatible agent. The agent reads the SKILL.md, explains what the tools do, dry-runs the install, and waits for your go-ahead before writing anything to disk. The README talks to AI now.

All technical content moved to TECHNICAL.md: Quick Start (6 steps), Agent Tools API (3 tools with parameter tables), Config Secret Resolution (with the `memorySearch.remote` gotcha), CLI Commands, Write Support, Configuration, How It Works, Security, Troubleshooting, and a full Developer Guide with three integration options and patterns for common scenarios.

New description: "Give your AI secure access to 1Password. Never copy-paste an API key into a chat window again."

**What changed:**
- `README.md` ... gutted from 400+ lines to ~75. Product page only. Badges, description, Teach Your AI block, What It Does (4 bullets), Documentation links, License.
- `TECHNICAL.md` ... expanded with all content from README plus new Developer Guide section (3 integration options, common patterns, key rules, example projects table).

---

### MCP server fixed

**The problem it solved:** `mcp-server.mjs` crashed on startup with "Schema is missing a method literal." The MCP server shipped in v0.2.0 but never actually worked. Every Claude Code user who tried to connect op-secrets hit this error.

**Root cause:** `setRequestHandler` was called with raw strings (`"tools/list"`, `"tools/call"`) instead of SDK schema objects. The MCP SDK validates the first argument against its type system and rejects strings.

**The fix:**
```javascript
// Before (broken)
server.setRequestHandler("tools/list", async () => ({...}));
server.setRequestHandler("tools/call", async (request) => ({...}));

// After (working)
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListToolsRequestSchema, async () => ({...}));
server.setRequestHandler(CallToolRequestSchema, async (request) => ({...}));
```

op-secrets MCP server now starts clean and connects to Claude Code.

**What changed:**
- `mcp-server.mjs` ... import `ListToolsRequestSchema` and `CallToolRequestSchema` from `@modelcontextprotocol/sdk/types.js`, replace string handlers with schema objects.

---

### 1Password plan requirements updated

Parker went back and forth with 1Password directly. The original docs said "Teams or Business" for service accounts. That was wrong.

**What we confirmed:**
- Service accounts work on **all plans**: Individual, Family, Teams, Business.
- Headless operation (no desktop app) confirmed on Teams and Business.
- Lower-tier plans may require the desktop app for initial setup.

**What changed:**
- `TECHNICAL.md` ... prerequisites section updated with correct plan requirements.
- `SKILL.md` ... compatibility line updated. Description changed from "OpenClaw plugin" to "AI plugin (Claude Code, OpenClaw)".

---

### Feature priority reordered

Agent tools (read/write secrets) is the #1 value of this tool. It was listed after config resolution. Reordered in both README and TECHNICAL.md:

1. Agent tools (read/write secrets on demand)
2. MCP server for Claude Code
3. Config resolution (`op://` refs at startup)
4. CLI diagnostics

---

### Files Changed

```
 README.md               | 389 ++----------------------------------------------
 SKILL.md                |   8 +-
 TECHNICAL.md            | 116 +++++++++++----
 mcp-server.mjs          |   5 +-
 5 files changed, 121 insertions(+), 412 deletions(-)
```

### Install

```bash
npm install -g @wipcomputer/wip-1password@0.2.2
```

Or update your local clone:
```bash
git pull origin main
```

---

Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).

## 0.2.1 (2026-03-12)

Restore full README with all documentation, fix broken Teach URL

## 0.2.0 (2026-03-12)

# v0.2.0 ... Full Treatment

The 1Password plugin gets the full WIP treatment: standard SKILL.md, license compliance, reformatted README, and a new MCP server for Claude Code.

## What's New

### MCP Server for Claude Code (#10)
New `mcp-server.mjs` provides 1Password access directly from Claude Code via MCP. Uses the `op` CLI with service account token for headless access. Three tools: `op_read_secret`, `op_list_items`, `op_test`.

Register with:
```bash
claude mcp add --scope user wip-1password -- node /path/to/mcp-server.mjs
```

### SKILL.md (agentskills.io spec) (#2)
Root SKILL.md with proper frontmatter: lowercase-hyphen name, metadata block with display-name/version/homepage/author, openclaw install instructions, capability list.

### License Compliance (#4)
Dual MIT+AGPL via `wip-license-guard --from-standard`. CLA.md generated. Attribution in README.

### README Reformatted (#3)
README reformatted to WIP Computer standard via `wip-readme-format`. Technical content (quick start, agent tools, config resolution, developer guide) split to TECHNICAL.md.

## Interfaces

| Interface | File | Status |
|-----------|------|--------|
| Module | `dist/index.js` | Existing |
| OpenClaw Plugin | `openclaw.plugin.json` | Existing |
| Skill | `skills/op-secrets/SKILL.md` | Existing |
| MCP Server | `mcp-server.mjs` | **New** |
| Root Skill | `SKILL.md` | **New** |

## Closes
#2, #3, #4, #7, #10



path: CLA.md
content: ###### WIP Computer

# Contributor License Agreement

By submitting a pull request to this repository, you agree to the following:

1. **You grant WIP Computer, Inc. a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license** to use, reproduce, modify, distribute, sublicense, and otherwise exploit your contribution under any license, including commercial licenses.

2. **You retain copyright** to your contribution. This agreement does not transfer ownership. You can use your own code however you want.

3. **You confirm** that your contribution is your original work, or that you have the right to submit it under these terms.

4. **You understand** that your contribution may be used in both open source and commercial versions of this software.

This is standard open source governance. Apache, Google, Meta, and Anthropic all use similar agreements. The goal is simple: keep the tools free for everyone while allowing WIP Computer, Inc. to offer commercial licenses to companies that need them.

Using these tools to build your own software is always free. This agreement only matters if WIP Computer, Inc. needs to relicense the codebase commercially.

If you have questions, open an issue or reach out.


path: README.md
content: ###### WIP Computer

[![Module](https://img.shields.io/badge/interface-Module-black)](#) [![MCP Server](https://img.shields.io/badge/interface-MCP_Server-black)](#) [![OpenClaw Plugin](https://img.shields.io/badge/interface-OpenClaw_Plugin-black)](#) [![Skill](https://img.shields.io/badge/interface-Skill-black)](#) [![Universal Interface Spec](https://img.shields.io/badge/Universal_Interface_Spec-black?style=flat&color=black)](https://github.com/wipcomputer/wip-ai-devops-toolbox/blob/main/tools/wip-universal-installer/SPEC.md)

# 1Password Secrets

Give your AI secure access to 1Password. Never copy-paste an API key into a chat window again. Your agent reads secrets directly from the vault at runtime.

Works with Claude Code, OpenClaw, and any MCP-compatible agent. Also ships as a Node.js module you can import into your own tools. Fully headless via service accounts... no desktop app, no biometrics, no popups.

## Teach Your AI to Use 1Password Secrets

Open your AI and say:

```
Read the SKILL.md at github.com/wipcomputer/wip-1password/blob/main/SKILL.md.

Then explain to me:
1. What are these tools?
2. What do they do?
3. What would they change about how we work together?

Then ask me:
- Do you have more questions?
- Do you want to install them?

If I say yes, run: wip-install wipcomputer/wip-1password --dry-run

Show me exactly what will change on my system. When I'm ready, I'll tell you
to install for real.
```

Your agent will read the repo, explain everything, and do a dry-run install first so you can see exactly what changes before anything is written to your system.

## What It Does

1. **Your agent can read and write secrets** ... API keys, tokens, credentials. No more pasting.
2. **Works in Claude Code out of the box** ... MCP server connects automatically.
3. **Secrets stay off disk** ... Config files use references, real values only exist in memory.
4. **Debug without exposing secrets** ... Test connectivity and preview values safely.

## Documentation

- [TECHNICAL.md](TECHNICAL.md) ... Full setup guide, agent tools API, config resolution, CLI commands, developer guide, troubleshooting
- [SKILL.md](SKILL.md) ... Machine-readable skill definition for AI agents
- [Universal Interface Spec](https://github.com/wipcomputer/wip-ai-devops-toolbox/blob/main/tools/wip-universal-installer/SPEC.md) ... The six interfaces every agent-native tool can ship

## License

Dual-license model designed to keep tools free while preventing commercial resellers.

```
MIT      All CLI tools, MCP servers, skills, and hooks (use anywhere, no restrictions).
AGPLv3   Commercial redistribution, marketplace listings, or bundling into paid services.
```

AGPLv3 for personal use is free. Commercial licenses available.

### Can I use this?

**Yes, freely:**
- Use any tool locally or on your own servers
- Modify the code for your own projects
- Include in your internal CI/CD pipelines
- Fork it and send us feedback via PRs (we'd love that)

**Need a commercial license:**
- Bundle into a product you sell
- List on a marketplace (VS Code, JetBrains, etc.)
- Offer as part of a hosted/SaaS platform
- Redistribute commercially

Using these tools to build your own software is fine. Reselling the tools themselves is what requires a commercial license.

By submitting a PR, you agree to the [Contributor License Agreement](CLA.md).

Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).


path: TECHNICAL.md
content: # 1Password Secrets ... Technical Documentation

## What it does

1. **Gives agents tools to read and write secrets on demand** ... Agents call `op_read_secret` to pull a key from 1Password at runtime, `op_list_items` to discover what's available, or `op_write_secret` to store new credentials.

2. **MCP server for Claude Code** ... `mcp-server.mjs` provides 1Password access directly from Claude Code via MCP. Uses the `op` CLI with service account token.

3. **Resolves `op://` references in `openclaw.json` at startup** ... Replace plaintext API keys with `op://Agent Secrets/Item/field` references. The plugin resolves them to real values in memory when OpenClaw boots. The plaintext key never touches disk.

4. **CLI for diagnostics** ... `openclaw op-secrets test` verifies 1Password connectivity. `openclaw op-secrets read` shows a redacted preview of any secret.

## Limitations

- **`auth-profiles.json` cannot use `op://` refs.** This file is loaded by a separate OpenClaw subsystem that the plugin can't hook into. Auth provider tokens (Anthropic, OpenAI OAuth) must remain as real values in that file. The resolver service only handles `openclaw.json` (the main config passed to plugins via `ctx.config`).

- **`op_write_secret` requires `write_items` permission** on the service account. The default setup uses `read_items` only. See [Write Support](#write-support) for setup.

- **Secrets are resolved in memory only.** If OpenClaw reloads config from disk mid-session, `op://` refs need to be resolved again. The service runs at startup, so this is fine for normal operation.

## Prerequisites

- **1Password account with service account support.** Service accounts are available on all plans (Individual, Family, Teams, Business). Headless operation (no desktop app) is confirmed on Teams and Business. Lower-tier plans may require the desktop app for initial setup.
- **Node.js 18+**
- **OpenClaw** 2026.1+

## Quick Start

### 1. Install the plugin

```bash
openclaw plugins install @wipcomputer/wip-1password
```

### 2. Set up 1Password

Create a custom vault and service account (service accounts can't access built-in vaults):

```bash
# Create a vault for agent secrets
op vault create "Agent Secrets"

# Add your API keys
op item create --category "API Credential" --title "OpenAI API" \
  --vault "Agent Secrets" 'api key=YOUR_KEY_HERE'

# Create a read-only service account
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items"
```

### 3. Save the service account token

```bash
mkdir -p ~/.openclaw/secrets
# Save the ops_... token from step 2 into this file:
echo "ops_..." > ~/.openclaw/secrets/op-sa-token
chmod 600 ~/.openclaw/secrets/op-sa-token
```

### 4. Enable the plugin

Add to `~/.openclaw/openclaw.json`:

```json
{
  "plugins": {
    "entries": {
      "op-secrets": {
        "enabled": true,
        "config": {
          "defaultVault": "Agent Secrets"
        }
      }
    }
  }
}
```

**Note:** The config key must be `op-secrets` (matching the plugin manifest `id`), not `1password`.

### 5. Test

```bash
openclaw op-secrets test
```

### 6. Using `op://` references in config

The plugin resolves `op://vault/item/field` strings in `openclaw.json` at startup. The real key never appears on disk.

**Example: a custom config field**
```json
{
  "someService": {
    "apiKey": "op://Agent Secrets/Some Service/api key"
  }
}
```

**Important: OpenAI API key for memory search**

Do **NOT** put an `apiKey` in `memorySearch.remote`. OpenClaw's `resolveOpenAiEmbeddingClient()` checks `remote.apiKey` first... if any value exists (even an `op://` reference), it uses it directly and **never** falls through to the env var. Instead, leave `"remote": {}` empty. The plugin resolves the OpenAI key from 1Password and sets `process.env.OPENAI_API_KEY` at startup; memory search picks it up from the env var.

```jsonc
// CORRECT ... env var fallback works
"memorySearch": { "remote": {} }

// WRONG ... blocks the env var fallback, key never resolves
"memorySearch": { "remote": { "apiKey": "op://Agent Secrets/OpenAI API/api key" } }
```

**Note:** `op://` resolution only works for `openclaw.json`. Auth profile tokens in `auth-profiles.json` cannot use `op://` refs... see [Limitations](#limitations).

## Agent Tools

### `op_read_secret`

Reads a secret value from 1Password. Agents call this tool when they need an API key or credential.

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `item` | Yes | | Item title (e.g. "OpenAI API") |
| `vault` | No | Config default | Vault name |
| `field` | No | `"api key"` | Field name |

### `op_list_items`

Lists all items in a vault so agents can discover available secrets.

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `vault` | No | Config default | Vault name |

### `op_write_secret`

Creates or updates a secret in 1Password. Requires `write_items` permission on the service account.

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `item` | Yes | | Item title (e.g. "New API Key") |
| `value` | Yes | | The secret value to store |
| `vault` | No | Config default | Vault name |
| `field` | No | `"api key"` | Field name |

All tools are registered as **optional** and must be allowlisted in agent config to use.

## Config Secret Resolution

The plugin registers a startup service that walks `openclaw.json` and resolves any string matching `op://vault/item/field` to its real value via the 1Password SDK. Resolution happens in memory... the file on disk keeps the `op://` reference.

The `resolveSecretRefs` utility is also exported for programmatic use:

```typescript
import { resolveSecretRefs } from "@wipcomputer/wip-1password";

const resolved = await resolveSecretRefs({
  [REDACTED_SECRET] Secrets/OpenAI API/api key",
  other: "not a secret, passed through",
});
// resolved.apiKey === "sk-proj-..."
// resolved.other === "not a secret, passed through"
```

## CLI Commands

```bash
# Verify 1Password connectivity
openclaw op-secrets test

# Read a secret (shows redacted preview, safe for terminals)
openclaw op-secrets read "OpenAI API"
openclaw op-secrets read "Anthropic Auth Token" --vault "Agent Secrets"
openclaw op-secrets read "DB Password" --field "password"

# Dry-run: show which op:// refs would be resolved in a JSON file
openclaw op-secrets resolve ~/.openclaw/openclaw.json
```

## Write Support

To enable write operations (`op_write_secret`), the service account needs `write_items` permission:

```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items,write_items"
```

Then update the token at `~/.openclaw/secrets/op-sa-token`.

## Configuration

Plugin config in `openclaw.json` under `plugins.entries.op-secrets.config`:

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `defaultVault` | string | `"Agent Secrets"` | Default vault for tool calls |
| `tokenPath` | string | `~/.openclaw/secrets/op-sa-token` | Path to service account token file |

The token path can also be set via the `OP_SA_TOKEN_PATH` environment variable.

## How It Works

1. On plugin load, the resolver service walks `openclaw.json` and resolves `op://` references in memory
2. On first secret request, the plugin reads the service account token from disk
3. It creates a 1Password SDK client (cached for the session lifetime)
4. Secrets are resolved via `client.secrets.resolve("op://Vault/Item/field")`
5. The SDK talks directly to 1Password's servers over HTTPS
6. Secret values exist in memory only... never written to disk or logs

The service account token does not expire (unless you set `--expires-in`). It survives reboots. No desktop app needed.

## Security

- Service account tokens grant access only to specific vaults you configure
- The token file is `chmod 600`... only the owning user can read it
- Secret values are never logged, cached, or written to disk by the plugin
- The SKILL.md file instructs agents to never echo or log returned secrets
- If a secret is exposed, rotate it in 1Password... the plugin always reads the current value
- `op://` references in config files are safe to commit to version control

## Troubleshooting

**"Token file not found"** ... Make sure `~/.openclaw/secrets/op-sa-token` exists and contains your `ops_...` token.

**"Vault not found"** ... Service accounts can only access custom vaults, not built-in ones (Shared, Employee, Private). Create a custom vault and grant access.

**"403 Forbidden" when creating service account** ... Enable "Developer" / "Secrets Automation" in your 1Password admin console under Settings.

**"Granting a service account access to the Team/Shared vault is not supported"** ... Use a custom vault name, not the built-in "Shared" vault.

**Write operations fail** ... The service account needs `write_items` permission. See [Write Support](#write-support).

## Developer Guide: Using 1Password in Your Own Projects

This section covers how to use 1Password secrets in new OpenClaw plugins, MCP servers, standalone scripts, or any Node.js project that needs secrets from the same vault.

### Option 1: Use the `op` CLI (simplest, any language)

Shell out to the `op` CLI with the service account token. This is what lesa-bridge uses.

```typescript
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";

const saToken = readFileSync(
  `${process.env.HOME}/.openclaw/secrets/op-sa-token`,
  "utf-8"
).trim();

const [REDACTED_SECRET]
  `op read "op://Agent Secrets/OpenAI API/api key"`,
  {
    env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
    encoding: "utf-8",
    timeout: 10000,
  }
).trim();
```

**Pros:** No native dependencies, works in any language, one-liner per secret.
**Cons:** Requires `op` CLI installed (`brew install 1password-cli`), subprocess overhead.

### Option 2: Use the 1Password JS SDK (this plugin's approach)

```typescript
import { createClient } from "@1password/sdk";
import { readFileSync } from "node:fs";

const [REDACTED_SECRET]
  `${process.env.HOME}/.openclaw/secrets/op-sa-token`,
  "utf-8"
).trim();

const client = await createClient({
  auth: token,
  integrationName: "MyProject",
  integrationVersion: "0.1.0",
});

// Resolve a secret
const apiKey = await client.secrets.resolve(
  "op://Agent Secrets/OpenAI API/api key"
);

// List vaults
const vaults = await client.vaults.list();

// List items in a vault
const items = await client.items.list(vaultId);
```

**Pros:** No subprocess, faster, full API (list/create/update items).
**Cons:** Native dependency (`@1password/sdk`), larger install footprint.

### Option 3: Use the plugin's tools at runtime (OpenClaw agents only)

If your code runs as an OpenClaw agent or plugin, call the existing `op_read_secret` tool:

```
op_read_secret({ item: "OpenAI API", vault: "Agent Secrets", field: "api key" })
```

No code needed... the tool is already registered by this plugin.

### Patterns for Common Scenarios

**Resolve a key at startup, cache it:**
```typescript
let cachedKey: string | null = null;

function getApiKey(): string {
  if (cachedKey) return cachedKey;
  cachedKey = execSync(`op read "op://Agent Secrets/MyService/api key"`, {
    env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
    encoding: "utf-8",
    timeout: 10000,
  }).trim();
  return cachedKey;
}
```

**Set as environment variable (for libraries that read `process.env`):**
```typescript
process.env.OPENAI_API_KEY = await client.secrets.resolve(
  "op://Agent Secrets/OpenAI API/api key"
);
// Now any library that checks process.env.OPENAI_API_KEY will find it
```

**Store a new secret (requires write_items permission):**
```typescript
await client.items.create({
  vaultId: "vault-uuid",
  title: "New API Key",
  category: ItemCategory.ApiCredential,
  fields: [
    {
      id: "api_key",
      title: "api key",
      value: "sk-proj-...",
      fieldType: ItemFieldType.Concealed,
    },
  ],
});
```

### Key Rules

1. **1Password account with service account support.** All plans offer service accounts. Headless confirmed on Teams and Business.
2. **Never hardcode secrets.** Use `op://` references in config, resolve at runtime.
3. **Never log secrets.** Use the `redact()` helper for debug output.
4. **Cache the client.** Creating a 1Password SDK client is expensive (~200ms). Create once, reuse.
5. **Cache resolved values.** Secrets don't change mid-session. Resolve once at startup.
6. **Service account token location:** Always `~/.openclaw/secrets/op-sa-token`. Don't invent new paths.
7. **Vault name:** `Agent Secrets` is the shared vault. Add items there unless you need isolation.
8. **Item naming:** Use descriptive titles like `"OpenAI API"`, `"GitHub Token"`. Only alphanumeric, `_`, `.`, `-` characters.
9. **SA permissions are immutable.** If you need `write_items` and the current SA only has `read_items`, you must create a new service account.

### Adding a New Secret

```bash
# Via op CLI
OP_SERVICE_ACCOUNT_TOKEN=$(cat ~/.openclaw/secrets/op-sa-token) \
  op item create --category "API Credential" --title "My New Service" \
  --vault "Agent Secrets" 'api key=YOUR_KEY_HERE'

# Verify
OP_SERVICE_ACCOUNT_TOKEN=$(cat ~/.openclaw/secrets/op-sa-token) \
  op read "op://Agent Secrets/My New Service/api key"
```

### Example Projects Using 1Password

| Project | How it uses 1Password |
|---------|----------------------|
| `wip-1password` (this) | JS SDK, startup resolver, agent tools |
| `lesa-bridge` | `op` CLI, resolves OpenAI key at MCP server startup |
| `context-embeddings` | Reads `process.env.OPENAI_API_KEY` (set by this plugin at boot) |
| `openclaw-tavily` | JS SDK, resolves Tavily API key at startup, sets `TAVILY_API_KEY` env var |


path: _trash/RELEASE-NOTES-v0-2-0.md
content: # v0.2.0 ... Full Treatment

The 1Password plugin gets the full WIP treatment: standard SKILL.md, license compliance, reformatted README, and a new MCP server for Claude Code.

## What's New

### MCP Server for Claude Code (#10)
New `mcp-server.mjs` provides 1Password access directly from Claude Code via MCP. Uses the `op` CLI with service account token for headless access. Three tools: `op_read_secret`, `op_list_items`, `op_test`.

Register with:
```bash
claude mcp add --scope user wip-1password -- node /path/to/mcp-server.mjs
```

### SKILL.md (agentskills.io spec) (#2)
Root SKILL.md with proper frontmatter: lowercase-hyphen name, metadata block with display-name/version/homepage/author, openclaw install instructions, capability list.

### License Compliance (#4)
Dual MIT+AGPL via `wip-license-guard --from-standard`. CLA.md generated. Attribution in README.

### README Reformatted (#3)
README reformatted to WIP Computer standard via `wip-readme-format`. Technical content (quick start, agent tools, config resolution, developer guide) split to TECHNICAL.md.

## Interfaces

| Interface | File | Status |
|-----------|------|--------|
| Module | `dist/index.js` | Existing |
| OpenClaw Plugin | `openclaw.plugin.json` | Existing |
| Skill | `skills/op-secrets/SKILL.md` | Existing |
| MCP Server | `mcp-server.mjs` | **New** |
| Root Skill | `SKILL.md` | **New** |

## Closes
#2, #3, #4, #7, #10


path: _trash/RELEASE-NOTES-v0-2-2.md
content: ## v0.2.2: AI-Native Homepage, MCP Fix, Plan Requirements

Ran wip-1password through the WIP DevOps toolbox and rebuilt the homepage from the ground up. The README is now an AI-compatible product page. All technical documentation lives in TECHNICAL.md. The MCP server that was crashing since v0.2.0 is fixed. 1Password plan requirements updated after direct conversations with 1Password support.

---

### README rebuilt as a product page

**The problem it solved:** The old README was 400+ lines of technical documentation. Quick Start, Agent Tools API, Config Resolution, CLI Commands, Write Support, Security, Troubleshooting, Developer Guide... all in one file. No human would read it. No agent could parse it efficiently.

**What changed:** The README is now a product page. One description, a "Teach Your AI" prompt block (Karpathy pattern), four human-readable bullets, and links to docs. That's it.

The "Teach Your AI" block is the key feature. You paste it into Claude Code, ChatGPT, or any MCP-compatible agent. The agent reads the SKILL.md, explains what the tools do, dry-runs the install, and waits for your go-ahead before writing anything to disk. The README talks to AI now.

All technical content moved to TECHNICAL.md: Quick Start (6 steps), Agent Tools API (3 tools with parameter tables), Config Secret Resolution (with the `memorySearch.remote` gotcha), CLI Commands, Write Support, Configuration, How It Works, Security, Troubleshooting, and a full Developer Guide with three integration options and patterns for common scenarios.

New description: "Give your AI secure access to 1Password. Never copy-paste an API key into a chat window again."

**What changed:**
- `README.md` ... gutted from 400+ lines to ~75. Product page only. Badges, description, Teach Your AI block, What It Does (4 bullets), Documentation links, License.
- `TECHNICAL.md` ... expanded with all content from README plus new Developer Guide section (3 integration options, common patterns, key rules, example projects table).

---

### MCP server fixed

**The problem it solved:** `mcp-server.mjs` crashed on startup with "Schema is missing a method literal." The MCP server shipped in v0.2.0 but never actually worked. Every Claude Code user who tried to connect op-secrets hit this error.

**Root cause:** `setRequestHandler` was called with raw strings (`"tools/list"`, `"tools/call"`) instead of SDK schema objects. The MCP SDK validates the first argument against its type system and rejects strings.

**The fix:**
```javascript
// Before (broken)
server.setRequestHandler("tools/list", async () => ({...}));
server.setRequestHandler("tools/call", async (request) => ({...}));

// After (working)
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListToolsRequestSchema, async () => ({...}));
server.setRequestHandler(CallToolRequestSchema, async (request) => ({...}));
```

op-secrets MCP server now starts clean and connects to Claude Code.

**What changed:**
- `mcp-server.mjs` ... import `ListToolsRequestSchema` and `CallToolRequestSchema` from `@modelcontextprotocol/sdk/types.js`, replace string handlers with schema objects.

---

### 1Password plan requirements updated

Parker went back and forth with 1Password directly. The original docs said "Teams or Business" for service accounts. That was wrong.

**What we confirmed:**
- Service accounts work on **all plans**: Individual, Family, Teams, Business.
- Headless operation (no desktop app) confirmed on Teams and Business.
- Lower-tier plans may require the desktop app for initial setup.

**What changed:**
- `TECHNICAL.md` ... prerequisites section updated with correct plan requirements.
- `SKILL.md` ... compatibility line updated. Description changed from "OpenClaw plugin" to "AI plugin (Claude Code, OpenClaw)".

---

### Feature priority reordered

Agent tools (read/write secrets) is the #1 value of this tool. It was listed after config resolution. Reordered in both README and TECHNICAL.md:

1. Agent tools (read/write secrets on demand)
2. MCP server for Claude Code
3. Config resolution (`op://` refs at startup)
4. CLI diagnostics

---

### Files Changed

```
 README.md               | 389 ++----------------------------------------------
 SKILL.md                |   8 +-
 TECHNICAL.md            | 116 +++++++++++----
 mcp-server.mjs          |   5 +-
 5 files changed, 121 insertions(+), 412 deletions(-)
```

### Install

```bash
npm install -g @wipcomputer/wip-1password@0.2.2
```

Or update your local clone:
```bash
git pull origin main
```

---

Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).


path: ai/_trash/README--before-format--2026-03-12.md
content: # @wipcomputer/wip-1password

OpenClaw plugin for 1Password secrets. Uses the official JavaScript SDK with service accounts for fully headless operation — no desktop app, no biometrics, no popups.

## What It Does

1. **Resolves `op://` references in `openclaw.json` at startup** — Replace plaintext API keys with `op://Agent Secrets/Item/field` references. The plugin resolves them to real values in memory when OpenClaw boots. The plaintext key never touches disk.

2. **Gives agents tools to read and write secrets on demand** — Agents call `op_read_secret` to pull a key from 1Password at runtime, `op_list_items` to discover what's available, or `op_write_secret` to store new credentials.

3. **CLI for diagnostics** — `openclaw op-secrets test` verifies 1Password connectivity. `openclaw op-secrets read` shows a redacted preview of any secret.

## Limitations

- **`auth-profiles.json` cannot use `op://` refs.** This file is loaded by a separate OpenClaw subsystem that the plugin can't hook into. Auth provider tokens (Anthropic, OpenAI OAuth) must remain as real values in that file. The resolver service only handles `openclaw.json` (the main config passed to plugins via `ctx.config`).

- **`op_write_secret` requires `write_items` permission** on the service account. The default setup uses `read_items` only. See [Write Support](#write-support) for setup.

- **Secrets are resolved in memory only.** If OpenClaw reloads config from disk mid-session, `op://` refs need to be resolved again. The service runs at startup, so this is fine for normal operation.

## Prerequisites

- **1Password Teams or Business plan** (required for service accounts)
- **1Password account with service account support** (Teams, Business, or Enterprise)
- **Node.js 18+**
- **OpenClaw** 2026.1+

## Quick Start

### 1. Install the plugin

```bash
openclaw plugins install @wipcomputer/wip-1password
```

### 2. Set up 1Password

Create a custom vault and service account (service accounts can't access built-in vaults):

```bash
# Create a vault for agent secrets
op vault create "Agent Secrets"

# Add your API keys
op item create --category "API Credential" --title "OpenAI API" \
  --vault "Agent Secrets" 'api key=YOUR_KEY_HERE'

# Create a read-only service account
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items"
```

### 3. Save the service account token

```bash
mkdir -p ~/.openclaw/secrets
# Save the ops_... token from step 2 into this file:
echo "ops_..." > ~/.openclaw/secrets/op-sa-token
chmod 600 ~/.openclaw/secrets/op-sa-token
```

### 4. Enable the plugin

Add to `~/.openclaw/openclaw.json`:

```json
{
  "plugins": {
    "entries": {
      "op-secrets": {
        "enabled": true,
        "config": {
          "defaultVault": "Agent Secrets"
        }
      }
    }
  }
}
```

**Note:** The config key must be `op-secrets` (matching the plugin manifest `id`), not `1password`.

### 5. Test

```bash
openclaw op-secrets test
```

### 6. Using `op://` references in config

The plugin resolves `op://vault/item/field` strings in `openclaw.json` at startup. The real key never appears on disk.

**Example: a custom config field**
```json
{
  "someService": {
    "apiKey": "op://Agent Secrets/Some Service/api key"
  }
}
```

**Important: OpenAI API key for memory search**

Do **NOT** put an `apiKey` in `memorySearch.remote`. OpenClaw's `resolveOpenAiEmbeddingClient()` checks `remote.apiKey` first — if any value exists (even an `op://` reference), it uses it directly and **never** falls through to the env var. Instead, leave `"remote": {}` empty. The plugin resolves the OpenAI key from 1Password and sets `process.env.OPENAI_API_KEY` at startup; memory search picks it up from the env var.

```jsonc
// CORRECT — env var fallback works
"memorySearch": { "remote": {} }

// WRONG — blocks the env var fallback, key never resolves
"memorySearch": { "remote": { "apiKey": "op://Agent Secrets/OpenAI API/api key" } }
```

**Note:** `op://` resolution only works for `openclaw.json`. Auth profile tokens in `auth-profiles.json` cannot use `op://` refs — see [Limitations](#limitations).

## Agent Tools

### `op_read_secret`

Reads a secret value from 1Password. Agents call this tool when they need an API key or credential.

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `item` | Yes | — | Item title (e.g. "OpenAI API") |
| `vault` | No | Config default | Vault name |
| `field` | No | `"api key"` | Field name |

### `op_list_items`

Lists all items in a vault so agents can discover available secrets.

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `vault` | No | Config default | Vault name |

### `op_write_secret`

Creates or updates a secret in 1Password. Requires `write_items` permission on the service account.

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `item` | Yes | — | Item title (e.g. "New API Key") |
| `value` | Yes | — | The secret value to store |
| `vault` | No | Config default | Vault name |
| `field` | No | `"api key"` | Field name |

All tools are registered as **optional** and must be allowlisted in agent config to use.

## Config Secret Resolution

The plugin registers a startup service that walks `openclaw.json` and resolves any string matching `op://vault/item/field` to its real value via the 1Password SDK. Resolution happens in memory — the file on disk keeps the `op://` reference.

The `resolveSecretRefs` utility is also exported for programmatic use:

```typescript
import { resolveSecretRefs } from "@wipcomputer/wip-1password";

const resolved = await resolveSecretRefs({
  [REDACTED_SECRET] Secrets/OpenAI API/api key",
  other: "not a secret, passed through",
});
// resolved.apiKey === "sk-proj-..."
// resolved.other === "not a secret, passed through"
```

## CLI Commands

```bash
# Verify 1Password connectivity
openclaw op-secrets test

# Read a secret (shows redacted preview, safe for terminals)
openclaw op-secrets read "OpenAI API"
openclaw op-secrets read "Anthropic Auth Token" --vault "Agent Secrets"
openclaw op-secrets read "DB Password" --field "password"

# Dry-run: show which op:// refs would be resolved in a JSON file
openclaw op-secrets resolve ~/.openclaw/openclaw.json
```

## Write Support

To enable write operations (`op_write_secret`), the service account needs `write_items` permission:

```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items,write_items"
```

Then update the token at `~/.openclaw/secrets/op-sa-token`.

## Configuration

Plugin config in `openclaw.json` under `plugins.entries.op-secrets.config`:

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `defaultVault` | string | `"Agent Secrets"` | Default vault for tool calls |
| `tokenPath` | string | `~/.openclaw/secrets/op-sa-token` | Path to service account token file |

The token path can also be set via the `OP_SA_TOKEN_PATH` environment variable.

## How It Works

1. On plugin load, the resolver service walks `openclaw.json` and resolves `op://` references in memory
2. On first secret request, the plugin reads the service account token from disk
3. It creates a 1Password SDK client (cached for the session lifetime)
4. Secrets are resolved via `client.secrets.resolve("op://Vault/Item/field")`
5. The SDK talks directly to 1Password's servers over HTTPS
6. Secret values exist in memory only — never written to disk or logs

The service account token does not expire (unless you set `--expires-in`). It survives reboots. No desktop app needed.

## Security

- Service account tokens grant access only to specific vaults you configure
- The token file is `chmod 600` — only the owning user can read it
- Secret values are never logged, cached, or written to disk by the plugin
- The SKILL.md file instructs agents to never echo or log returned secrets
- If a secret is exposed, rotate it in 1Password — the plugin always reads the current value
- `op://` references in config files are safe to commit to version control

## Troubleshooting

**"Token file not found"** — Make sure `~/.openclaw/secrets/op-sa-token` exists and contains your `ops_...` token.

**"Vault not found"** — Service accounts can only access custom vaults, not built-in ones (Shared, Employee, Private). Create a custom vault and grant access.

**"403 Forbidden" when creating service account** — Enable "Developer" / "Secrets Automation" in your 1Password admin console under Settings.

**"Granting a service account access to the Team/Shared vault is not supported"** — Use a custom vault name, not the built-in "Shared" vault.

**Write operations fail** — The service account needs `write_items` permission. See [Write Support](#write-support).

## Developer Guide: Using 1Password in Your Own Projects

This section covers how to use 1Password secrets in new OpenClaw plugins, MCP servers, standalone scripts, or any Node.js project that needs secrets from the same vault.

### Option 1: Use the `op` CLI (simplest, any language)

Shell out to the `op` CLI with the service account token. This is what lesa-bridge uses.

```typescript
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";

const saToken = readFileSync(
  `${process.env.HOME}/.openclaw/secrets/op-sa-token`,
  "utf-8"
).trim();

const [REDACTED_SECRET]
  `op read "op://Agent Secrets/OpenAI API/api key"`,
  {
    env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
    encoding: "utf-8",
    timeout: 10000,
  }
).trim();
```

**Pros:** No native dependencies, works in any language, one-liner per secret.
**Cons:** Requires `op` CLI installed (`brew install 1password-cli`), subprocess overhead.

### Option 2: Use the 1Password JS SDK (this plugin's approach)

```typescript
import { createClient } from "@1password/sdk";
import { readFileSync } from "node:fs";

const [REDACTED_SECRET]
  `${process.env.HOME}/.openclaw/secrets/op-sa-token`,
  "utf-8"
).trim();

const client = await createClient({
  auth: token,
  integrationName: "MyProject",
  integrationVersion: "0.1.0",
});

// Resolve a secret
const apiKey = await client.secrets.resolve(
  "op://Agent Secrets/OpenAI API/api key"
);

// List vaults
const vaults = await client.vaults.list();

// List items in a vault
const items = await client.items.list(vaultId);
```

**Pros:** No subprocess, faster, full API (list/create/update items).
**Cons:** Native dependency (`@1password/sdk`), larger install footprint.

### Option 3: Use the plugin's tools at runtime (OpenClaw agents only)

If your code runs as an OpenClaw agent or plugin, call the existing `op_read_secret` tool:

```
op_read_secret({ item: "OpenAI API", vault: "Agent Secrets", field: "api key" })
```

No code needed — the tool is already registered by this plugin.

### Patterns for Common Scenarios

**Resolve a key at startup, cache it:**
```typescript
let cachedKey: string | null = null;

function getApiKey(): string {
  if (cachedKey) return cachedKey;
  cachedKey = execSync(`op read "op://Agent Secrets/MyService/api key"`, {
    env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
    encoding: "utf-8",
    timeout: 10000,
  }).trim();
  return cachedKey;
}
```

**Set as environment variable (for libraries that read `process.env`):**
```typescript
process.env.OPENAI_API_KEY = await client.secrets.resolve(
  "op://Agent Secrets/OpenAI API/api key"
);
// Now any library that checks process.env.OPENAI_API_KEY will find it
```

**Store a new secret (requires write_items permission):**
```typescript
await client.items.create({
  vaultId: "vault-uuid",
  title: "New API Key",
  category: ItemCategory.ApiCredential,
  fields: [
    {
      id: "api_key",
      title: "api key",
      value: "sk-proj-...",
      fieldType: ItemFieldType.Concealed,
    },
  ],
});
```

### Key Rules

1. **Use your 1Password account with service account support** (Teams, Business, or Enterprise).
2. **Never hardcode secrets.** Use `op://` references in config, resolve at runtime.
2. **Never log secrets.** Use the `redact()` helper for debug output.
3. **Cache the client.** Creating a 1Password SDK client is expensive (~200ms). Create once, reuse.
4. **Cache resolved values.** Secrets don't change mid-session. Resolve once at startup.
5. **Service account token location:** Always `~/.openclaw/secrets/op-sa-token`. Don't invent new paths.
6. **Vault name:** `Agent Secrets` is the shared vault. Add items there unless you need isolation.
7. **Item naming:** Use descriptive titles like `"OpenAI API"`, `"GitHub Token"`. Only alphanumeric, `_`, `.`, `-` characters.
8. **SA permissions are immutable.** If you need `write_items` and the current SA only has `read_items`, you must create a new service account.

### Adding a New Secret

```bash
# Via op CLI
OP_SERVICE_ACCOUNT_TOKEN=$(cat ~/.openclaw/secrets/op-sa-token) \
  op item create --category "API Credential" --title "My New Service" \
  --vault "Agent Secrets" 'api key=YOUR_KEY_HERE'

# Verify
OP_SERVICE_ACCOUNT_TOKEN=$(cat ~/.openclaw/secrets/op-sa-token) \
  op read "op://Agent Secrets/My New Service/api key"
```

### Example Projects Using 1Password

| Project | How it uses 1Password |
|---------|----------------------|
| `openclaw-1password` (this) | JS SDK, startup resolver, agent tools |
| `lesa-bridge` | `op` CLI, resolves OpenAI key at MCP server startup |
| `openclaw-context-embeddings` | Reads `process.env.OPENAI_API_KEY` (set by this plugin at boot) |
| `openclaw-tavily` | JS SDK, resolves Tavily API key at startup, sets `TAVILY_API_KEY` env var |

## License

MIT


path: ai/dev-updates/2026-03-11--cc-mini--full-treatment.md
content: # Dev Update: Full Treatment

**Date:** 2026-03-11
**Author:** CC-Mini
**Version:** 0.1.0 -> 0.2.0

## What Changed

- **Root SKILL.md** added, conforming to agentskills.io spec (lowercase-hyphen name, metadata block, openclaw install instructions)
- **README reformatted** to WIP Computer standard via `wip-readme-format`. Technical content split to `TECHNICAL.md`.
- **License compliance**: dual MIT+AGPL via `wip-license-guard --from-standard`. CLA.md generated. Attribution added.
- **MCP server** (`mcp-server.mjs`) added for Claude Code access. Uses `op` CLI with service account token. Provides `op_read_secret`, `op_list_items`, `op_test` tools.
- **MCP SDK** added as dependency.

## Why

Part of the distribution fix initiative across all WIP repos. Every tool should have: SKILL.md, license compliance, standard README, npm publish, public repo sync. This repo was missing all of them.

## What's Next

- npm publish from public repo (deploy-public.sh handles this)
- ClawHub publish (blocked by upstream bug #109)
- Consider promoting MCP server to Claude Code MCP registry


path: ai/dev-updates/2026-03-11--cc-mini--readme-overhaul.md
content: # Dev Update: README Overhaul + MCP Fix

**Date:** 2026-03-11
**Author:** Claude Code (cc-mini)
**Branch:** cc-mini/dev

## What Changed

### README restructured
- Stripped all technical content out of README. It's now a product page: description, teach section, what it does (4 human-readable bullets), docs links, license.
- All technical docs (Quick Start, Agent Tools API, Config Resolution, CLI Commands, Write Support, Configuration, How It Works, Security, Troubleshooting, Developer Guide) live in TECHNICAL.md.
- New description: "Give your AI secure access to 1Password. Never copy-paste an API key into a chat window again."
- Compatibility line: works with Claude Code, OpenClaw, and any MCP-compatible agent. Also ships as Node.js module.

### MCP server fixed
- `mcp-server.mjs` was crashing on startup with "Schema is missing a method literal"
- Root cause: `setRequestHandler("tools/list", ...)` used strings instead of SDK schema objects
- Fix: import `ListToolsRequestSchema` and `CallToolRequestSchema` from `@modelcontextprotocol/sdk/types.js`
- op-secrets MCP server now starts clean and connects to Claude Code

### 1Password plan requirements clarified
- Service accounts work on all plans (Individual, Family, Teams, Business)
- Headless operation confirmed on Teams and Business
- Lower-tier plans may require desktop app for initial setup
- Updated TECHNICAL.md prerequisites and SKILL.md compatibility

### Feature priority reordered
- Agent tools (read/write secrets) is now #1, not config resolver
- MCP server is #2
- Reflects actual primary value: agents accessing secrets at runtime

## Commits on cc-mini/dev
1. Update description: Give your AI secure access to 1Password
2. Simplify What It Does to one-liners, details in TECHNICAL.md
3. Update description with clear value prop and compatibility
4. Remove What It Does section from README, lives in TECHNICAL.md
5. Add human-readable What It Does section
6. Move What It Does below Teach Your AI section
7. Service accounts work on all 1Password plans
8. Clarify 1Password plan requirements: all plans, headless confirmed on Teams+

## Issues Filed (on wip-ai-devops-toolbox-private)
- #123 deploy-public: require explicit approval before running
- #124 Branch deletion must verify merged status before deleting
- #125 gh pr merge should always use --delete-branch
- #126 wip-release: block if stale remote branches exist

## Still Open
- #5 Publish to ClawHub (rate limited, retry later)


path: ai/todos/PUNCHLIST.md
content: # Punchlist

No current blockers.


path: docs/DEVELOPMENT.md
content: # Development Guide: openclaw-1password

Step-by-step instructions for building this plugin from scratch. Each step is self-contained — an agent or developer can follow them sequentially.

---

## Prerequisites

Before starting, confirm:

- Node.js 18+ installed
- OpenClaw installed (`openclaw --version`)
- A 1Password Business or Teams account
- A 1Password service account token (see [1Password Setup](#step-0-1password-setup))

---

## Step 0: 1Password Setup

This is a one-time admin task, not code.

### 0.1 Create a custom vault

Service accounts **cannot** access built-in vaults (Shared, Employee, Private). You must create a custom vault.

```bash
op vault create "Agent Secrets" --description "API keys for OpenClaw agents"
```

### 0.2 Add secrets to the vault

```bash
op item create --category "API Credential" --title "OpenAI API" \
  --vault "Agent Secrets" 'api key=sk-...'

op item create --category "API Credential" --title "Anthropic Auth Token" \
  --vault "Agent Secrets" 'api key=sk-ant-...'
```

### 0.3 Create a service account

```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items"
```

This outputs a token starting with `ops_...`. Save it:

```bash
mkdir -p ~/.openclaw/secrets
# Paste the token into this file:
nano ~/.openclaw/secrets/op-sa-token
chmod 600 ~/.openclaw/secrets/op-sa-token
```

### 0.4 Verify

```bash
export OP_SERVICE_ACCOUNT_TOKEN=$(cat ~/.openclaw/secrets/op-sa-token)
op vault list     # should show "Agent Secrets"
op item list --vault "Agent Secrets"  # should show your items
unset OP_SERVICE_ACCOUNT_TOKEN
```

---

## Step 1: Project Scaffolding

### 1.1 Initialize the project

```bash
mkdir openclaw-1password && cd openclaw-1password
git init
npm init -y
```

### 1.2 Create `package.json`

Overwrite the generated `package.json` with:

```json
{
  "name": "@wipcomputer/wip-1password",
  "version": "0.1.0",
  "description": "OpenClaw plugin for 1Password secrets via JS SDK",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist", "skills", "openclaw.plugin.json"],
  "openclaw": {
    "extensions": ["./dist/index.js"]
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "dev": "tsup src/index.ts --format esm --dts --watch",
    "prepublishOnly": "npm run build"
  },
  "keywords": ["openclaw", "openclaw-plugin", "1password", "secrets"],
  "license": "MIT",
  "peerDependencies": {
    "openclaw": ">=2026.1.0"
  },
  "dependencies": {
    "@1password/sdk": "^0.3.1"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.4.0",
    "openclaw": "*"
  }
}
```

Key points:
- `"openclaw.extensions"` tells OpenClaw where to find the plugin entry point
- `"files"` controls what gets published to npm
- `tsup` bundles TypeScript to ESM for distribution
- `openclaw` is a peer dependency (the host provides it)

### 1.3 Create `tsconfig.json`

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
```

### 1.4 Create `openclaw.plugin.json`

```json
{
  "id": "1password",
  "name": "1Password Secrets",
  "description": "Read secrets from 1Password via JS SDK (headless, service account)",
  "skills": ["./skills"],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "defaultVault": { "type": "string" },
      "tokenPath": { "type": "string" }
    }
  },
  "uiHints": {
    "defaultVault": {
      "label": "Default Vault",
      "placeholder": "Agent Secrets"
    },
    "tokenPath": {
      "label": "Service Account Token Path",
      "placeholder": "~/.openclaw/secrets/op-sa-token",
      "sensitive": true
    }
  }
}
```

### 1.5 Create `.gitignore`

```
node_modules/
dist/
*.tgz
```

### 1.6 Install dependencies

```bash
npm install
```

---

## Step 2: Plugin Entry Point (`src/index.ts`)

This is the core of the plugin. It has three parts:

### 2.1 SDK Client (lazy singleton)

```typescript
import { readFileSync } from "node:fs";
import { createClient, type Client } from "@1password/sdk";
```

Create a `getClient()` function that:
- Reads the service account token from the configured file path
- Calls `createClient({ auth: token, integrationName: "OpenClaw", integrationVersion: "<pkg version>" })`
- Caches the client in a module-level variable (lazy singleton)
- Throws a clear error if the token file doesn't exist

### 2.2 Helper functions

Create three helpers:

1. **`resolveSecret(vault, item, field)`** — Calls `client.secrets.resolve(\`op://${vault}/${item}/${field}\`)`. This is the SDK's secret reference format.

2. **`listVaults()`** — Calls `client.vaults.list()`. Returns an array of vault objects with `id` and `title`.

3. **`listItems(vaultId)`** — Calls `client.items.list(vaultId)`. Returns an array of item objects with `id`, `title`, and `category`.

### 2.3 Plugin registration

Export a default plugin object with:
- `id`, `name`, `description`, `configSchema` (matching the manifest)
- `register(api)` function that:

**Reads config:**
```typescript
const pluginConfig = (api.config as any)?.plugins?.entries?.["1password"]?.config;
const defaultVault = pluginConfig?.defaultVault || "Agent Secrets";
const tokenPath = pluginConfig?.tokenPath || `${process.env.HOME}/.openclaw/secrets/op-sa-token`;
```

**Registers `op_read_secret` tool:**
- Parameters: `item` (required string), `vault` (optional string), `field` (optional string, default `"api key"`)
- Execute: calls `resolveSecret()`, returns value as text content
- Registered as `{ optional: true }` — requires allowlisting
- Error handling: catch and return `{ isError: true }` with message

**Registers `op_list_items` tool:**
- Parameters: `vault` (optional string)
- Execute: finds vault by name, calls `listItems(vault.id)`, formats as `title (category)` lines
- Registered as `{ optional: true }`

**Registers CLI commands:**
- `openclaw op-secrets test` — calls `listVaults()`, prints vault names
- `openclaw op-secrets read <item>` — calls `resolveSecret()`, prints redacted preview (first 6 chars + length + last 4 chars)

### 2.4 `resolveSecretRefs` utility

Create and export a `resolveSecretRefs<T>(obj: T): Promise<T>` function that:
- Recursively walks any JS object
- For any string matching `op://vault/item/field`, resolves it via `resolveSecret()`
- Returns a new object with resolved values (non-matching values pass through)
- Handles arrays and nested objects

This is the core utility that the startup service uses, and is also exported for programmatic use by other plugins.

### 2.5 Write tool (`op_write_secret`)

Registers an `op_write_secret` tool that:
- Parameters: `item` (required), `value` (required), `vault` (optional), `field` (optional, default `"api key"`)
- Checks if item already exists in the vault (case-insensitive title match)
- If exists: gets the full item, finds/updates the field, puts it back via `client.items.put()`
- If new: creates via `client.items.create()` with `ItemCategory.ApiCredentials` and `ItemFieldType.Concealed`
- Registered as `{ optional: true }`
- Requires `write_items` permission on the service account

### 2.6 Startup service (`op-secret-resolver`)

Register a service via `api.registerService()` that runs at OpenClaw startup:
```typescript
api.registerService?.({
  id: "op-secret-resolver",
  async start(ctx: any) {
    const resolved = await resolveSecretRefs(ctx.config);
    Object.assign(ctx.config, resolved);
  },
});
```

This resolves `op://` references in `openclaw.json` (the main config). Note:
- Uses `Object.assign` for shallow merge — works because `resolveSecretRefs` returns a full deep copy
- Only resolves `ctx.config` — cannot resolve `auth-profiles.json` (separate subsystem)
- Runs before agents start, so resolved values are available immediately

### 2.7 CLI resolve command

Add a third CLI command `openclaw op-secrets resolve <file>` that:
- Reads a JSON file, resolves `op://` refs (dry run)
- Shows which keys were resolved with redacted values
- Useful for verifying which refs exist in a config file

---

## Step 3: Skill File (`skills/op-secrets/SKILL.md`)

Create `skills/op-secrets/SKILL.md` with YAML frontmatter:

```yaml
---
name: op-secrets
description: Read secrets from 1Password using the op-secrets plugin tools.
metadata:
  openclaw:
    emoji: "\U0001F510"
    requires:
      plugins: ["1password"]
---
```

The body should document:
- Both tools and their parameters
- Example items that might be in the vault
- **Guardrails**: Never log, echo, or paste secret values into chat/logs/code. Store in variables only.

---

## Step 4: Build and Test Locally

### 4.1 Build

```bash
npm run build
```

Verify `dist/index.js` and `dist/index.d.ts` exist.

### 4.2 Link for local testing

```bash
# Copy to extensions directory
cp -r . ~/.openclaw/extensions/1password/

# Or symlink for development
ln -sf "$(pwd)" ~/.openclaw/extensions/1password
```

### 4.3 Enable in config

Add to `~/.openclaw/openclaw.json`:

```json
{
  "plugins": {
    "entries": {
      "1password": {
        "enabled": true,
        "config": {
          "defaultVault": "Agent Secrets"
        }
      }
    }
  }
}
```

### 4.4 Test

```bash
openclaw op-secrets test
# Expected: "1Password connection OK — 1 vault(s): Agent Secrets (id)"

openclaw op-secrets read "OpenAI API"
# Expected: "Agent Secrets/OpenAI API/api key: sk-pro...[154 chars]...EEwA"
```

---

## Step 5: Publish to npm

### 5.1 Ensure build is fresh

```bash
npm run build
```

### 5.2 Verify package contents

```bash
npm pack --dry-run
```

Should show: `dist/`, `skills/`, `openclaw.plugin.json`, `package.json`, `README.md`

### 5.3 Publish

```bash
npm publish --access public
```

### 5.4 Verify installation

```bash
openclaw plugins install @wipcomputer/wip-1password
openclaw op-secrets test
```

---

## File Inventory

After completing all steps, the project should contain:

```
openclaw-1password/
├── .gitignore
├── package.json              # npm metadata, scripts, openclaw.extensions
├── tsconfig.json             # TypeScript config
├── openclaw.plugin.json      # OpenClaw plugin manifest
├── README.md                 # User-facing docs
├── LICENSE                   # MIT
├── docs/
│   ├── PRD.md                # Product requirements
│   └── DEVELOPMENT.md        # This file
├── src/
│   └── index.ts              # Plugin code (~480 lines)
├── skills/
│   └── op-secrets/
│       └── SKILL.md          # Agent-facing skill documentation
└── dist/                     # Built output (git-ignored)
    ├── index.js
    └── index.d.ts
```

---

## Key API Reference

### 1Password SDK

```typescript
import { createClient } from "@1password/sdk";

const client = await createClient({
  auth: "ops_...",
  integrationName: "OpenClaw",
  integrationVersion: "0.1.0",
});

// Resolve a secret by reference
const value = await client.secrets.resolve("op://Vault Name/Item Title/field name");

// List vaults
const vaults = await client.vaults.list();
// Returns: [{ id: string, title: string, createdAt: string, updatedAt: string }]

// List items in a vault
const items = await client.items.list(vaultId);
// Returns: [{ id: string, title: string, category: string, ... }]
```

### OpenClaw Plugin SDK

```typescript
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

// Register an agent tool
api.registerTool(
  {
    name: "tool_name",
    description: "What it does",
    parameters: { type: "object", properties: { ... }, required: [...] },
    async execute(_id, params) {
      return { content: [{ type: "text", text: "result" }] };
    },
  },
  { optional: true },
);

// Register CLI commands
api.registerCli(
  ({ program }) => {
    program.command("my-cmd").description("...").action(async () => { ... });
  },
  { commands: ["my-cmd"] },
);

// Access plugin config
const cfg = (api.config as any)?.plugins?.entries?.["plugin-id"]?.config;
```


path: docs/HANDOFF.md
content: # Status: openclaw-1password

This document tracks what's been done and what remains.

---

## Completed

### Plugin core (all working)
- `op_read_secret` tool — tested, working
- `op_list_items` tool — tested, working
- `op_write_secret` tool — implemented, **not yet tested** (service account may need `write_items`)
- CLI: `test`, `read`, `resolve` commands — all working
- `resolveSecretRefs` utility — working, exported
- Lazy SDK client with token validation — working
- SKILL.md — written

### Config secret resolution
- Startup service (`op-secret-resolver`) resolves `op://` refs in `openclaw.json`
- `openclaw.json` uses `op://Agent Secrets/OpenAI API/api key` for the OpenAI key
- Resolved in memory at boot — plaintext never on disk

### Infrastructure
- 1Password Business plan on `wipcomputer.1password.com`
- Custom "Agent Secrets" vault (ID: `ywmget6o6aki6wh2e4slzvrr5a`)
- Service account "OpenClaw Agent" with `read_items` access
- Token at `~/.openclaw/secrets/op-sa-token` (chmod 600)

---

## Remaining

### 1. Test `op_write_secret` against real vault

The write tool is implemented but never tested live.

**Steps:**
1. Verify connectivity: `openclaw op-secrets test`
2. Test creating a new item via agent tool `op_write_secret`:
   - `item`: "Test Secret", `value`: "test-value-12345"
3. Verify: `openclaw op-secrets read "Test Secret"`
4. Test updating: call `op_write_secret` again with `value`: "updated-value-67890"
5. Clean up: delete "Test Secret" from 1Password UI

**If it fails:** The service account likely needs `write_items`. Recreate it:
```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items,write_items" \
  --account wipcomputer.1password.com
```
Then update the token at `~/.openclaw/secrets/op-sa-token`.

### 2. Publish to npm (optional)

The package is structured for npm publishing but not yet published.

```bash
cd /Users/lesa/Documents/Projects/Claude\ Code/openclaw-1password
npm run build
npm publish --access public
```

Requires an npm account with a scope (e.g. `@openclaw`).

---

## Known Limitation: auth-profiles.json

`auth-profiles.json` is loaded by a separate OpenClaw subsystem that plugins cannot hook into. The resolver service only has access to `ctx.config` (which corresponds to `openclaw.json`).

**Consequence:** The Anthropic token in `auth-profiles.json` must remain as plaintext. It cannot use `op://` references.

**To fix this properly:** OpenClaw core would need to support a "secret provider" hook in its auth profile loader. This is a feature request for OpenClaw, not something the plugin can solve.

**Current state of secrets on disk:**

| File | Secret | Storage | Status |
|------|--------|---------|--------|
| `openclaw.json` | OpenAI API key | `op://` reference | No plaintext on disk |
| `auth-profiles.json` | Anthropic token | Plaintext | Cannot use `op://` |
| `auth-profiles.json` | OpenAI OAuth tokens | Plaintext (auto-managed) | OAuth, not static secrets |

---

## Reference

### Service account
- **Account**: wipcomputer.1password.com
- **Service account**: "OpenClaw Agent"
- **Token**: `~/.openclaw/secrets/op-sa-token` (chmod 600)
- **Vault**: "Agent Secrets" (custom vault)
- **Permissions**: `read_items` (may need `write_items` for write tool)

### Secrets in vault
| Item | Field | What it is |
|------|-------|------------|
| OpenAI API | api key | OpenAI project API key |
| Anthropic Auth Token | api key | Anthropic auth token |

### File locations
- **Repo**: `/Users/lesa/Documents/Projects/Claude Code/openclaw-1password/`
- **Live extension**: `~/.openclaw/extensions/op-secrets/` (loads `index.ts` directly)
- **Source**: `src/index.ts`

### Live extension vs repo

The live extension at `~/.openclaw/extensions/op-secrets/` uses plugin ID `"op-secrets"` and config key `plugins.entries["op-secrets"]`. The repo uses plugin ID `"1password"` and config key `plugins.entries["1password"]`. These are independent — the live extension is a local install, the repo is for distribution.


path: docs/PRD.md
content: # PRD: openclaw-1password

## Problem

OpenClaw agents need API keys and secrets (Anthropic, OpenAI, etc.) to function. Today, these credentials are stored as **plaintext** in configuration files:

- `openclaw.json` (e.g., `memorySearch.remote.apiKey`)
- `agents/main/agent/auth-profiles.json` (provider tokens)
- `.env` files or environment variables

This creates several risks:

1. **Leakage into logs**: Agent session logs (`.jsonl`) capture tool calls and responses. Secrets passed through config end up serialized into these logs in plaintext.
2. **No rotation**: Changing a key means editing config files manually across every machine.
3. **No audit trail**: No record of which agent accessed which secret, when.
4. **No access control**: Any process running as the user can read the config files.

## Solution

An OpenClaw plugin (`@wipcomputer/wip-1password`) that integrates with **1Password** via the official JavaScript SDK. Agents retrieve secrets at runtime from a 1Password vault using a service account — fully headless, no desktop app or biometric prompts required.

## Target Users

- OpenClaw users who manage API keys for multiple providers
- Teams running OpenClaw agents on servers, CI/CD, or unattended machines
- Anyone who wants to stop storing plaintext secrets in config files

## Requirements

### Implemented (P0)

1. **Agent tool: `op_read_secret`** — Reads a single secret value from 1Password given vault, item, and field names. Returns the secret to the agent in memory (never written to disk).

2. **Agent tool: `op_list_items`** — Lists items in a vault so agents can discover available secrets.

3. **Agent tool: `op_write_secret`** — Creates or updates a secret in 1Password. Supports both creating new items and updating existing fields. Requires `write_items` permission on the service account.

4. **Service account auth** — Uses a 1Password service account token for headless operation. Token stored in a local file with `600` permissions.

5. **CLI commands** — `openclaw op-secrets test` (verify connectivity), `openclaw op-secrets read <item>` (read with redacted preview), `openclaw op-secrets resolve <file>` (dry-run showing which `op://` refs would resolve).

6. **Skill file** — `SKILL.md` that teaches agents what tools are available and the guardrails (never log secret values).

7. **Config secret resolution** — A startup service that resolves `op://vault/item/field` references in `openclaw.json` to real values in memory. Enables replacing plaintext API keys in config with `op://` URIs.

8. **`resolveSecretRefs` utility** — Exported function that walks any JS object tree and resolves `op://` strings. Usable by other plugins or scripts.

9. **Configurable token path** — Token file location configurable via plugin config or `OP_SA_TOKEN_PATH` environment variable.

10. **Configurable default vault** — Default vault name set in plugin config, overridable per tool call.

11. **Lazy SDK client** — The 1Password SDK client is initialized on first use, not at plugin load time. Avoids blocking startup if the token is missing.

12. **Graceful degradation** — If the token file is missing or invalid, tools return clear error messages instead of crashing the gateway.

### Not Possible Without Core Changes

13. **Auth profile secret resolution** — `auth-profiles.json` is loaded by a separate OpenClaw subsystem that plugins cannot hook into. Secrets in that file (Anthropic token, OpenAI OAuth tokens) must remain as plaintext. Fixing this requires OpenClaw core to support a "secret provider" hook in its auth profile loader.

### Nice to Have (P2)

14. **npm-publishable** — Installable via `openclaw plugins install @wipcomputer/wip-1password`. Package structure is ready but not yet published.

15. **Multiple vault support** — Service account with access to multiple vaults (already supported by the code, just needs the SA configured with access to multiple vaults).

## Architecture

```
1Password Cloud
    ^
    |  HTTPS (SDK)
    |
@1password/sdk client  (singleton, lazy-init)
    ^
    |  reads token from file
    |
~/.openclaw/secrets/op-sa-token  (chmod 600)
    |
openclaw-1password plugin
    |
    +-- op_read_secret tool     (agent-facing)
    +-- op_list_items tool      (agent-facing)
    +-- op_write_secret tool    (agent-facing, needs write_items)
    +-- op-secret-resolver      (startup service, resolves op:// in config)
    +-- openclaw op-secrets     (CLI for human debugging)
    +-- resolveSecretRefs()     (exported utility)
    +-- SKILL.md                (agent knowledge)
```

### Scope of Secret Resolution

```
openclaw.json (ctx.config)
  └── Any op:// string → ✅ Resolved at startup by plugin service

auth-profiles.json
  └── Provider tokens → ❌ Cannot resolve (separate subsystem)
                          Requires OpenClaw core "secret provider" hook
```

### 1Password Setup (User Responsibility)

1. 1Password **Business** or **Teams** plan (required for service accounts)
2. A **custom vault** (e.g., "Agent Secrets") — service accounts cannot access built-in Shared/Employee vaults
3. A **service account** with `read_items` access (add `write_items` for write support)
4. Service account token saved to `~/.openclaw/secrets/op-sa-token`

### Plugin Lifecycle

1. **Load**: OpenClaw discovers plugin via `openclaw.plugin.json` manifest
2. **Register**: Plugin registers tools, CLI commands, and startup service
3. **Startup service**: Walks `ctx.config`, resolves `op://` refs in memory
4. **First tool use**: On first `op_read_secret` call, reads token from disk, creates SDK client (if not already created by the resolver)
5. **Resolve**: Calls `client.secrets.resolve("op://<vault>/<item>/<field>")`
6. **Return**: Secret value returned to agent in tool response (in memory only)

## Non-Goals

- **Desktop app integration**: We explicitly avoid the `op` CLI and desktop app. The whole point is headless operation.
- **Token generation**: The plugin does not create service accounts. That's a one-time admin setup.
- **Secret rotation**: Out of scope. Users rotate secrets in 1Password; the plugin always reads the current value.
- **Caching**: Secrets are fetched fresh on every call. 1Password SDK handles connection pooling internally.
- **Auth profile resolution**: Out of scope for the plugin. Requires OpenClaw core changes.

## Success Criteria

1. `openclaw op-secrets test` shows vault connectivity
2. Agent calls `op_read_secret` and receives the correct value
3. `openclaw.json` can use `op://` references instead of plaintext keys
4. No secret values appear in session logs, config files on disk, or CLI output
5. Plugin works after reboot with no human intervention
6. Plugin loads in <500ms, secret resolution in <2s

## Dependencies

- `@1password/sdk` (^0.3.x) — 1Password JavaScript SDK
- Node.js 18+ (SDK requirement)
- OpenClaw plugin SDK (`openclaw/plugin-sdk`)


path: docs/SETUP.md
content: # Setup Guide: 1Password for OpenClaw

Step-by-step instructions for setting up 1Password integration with OpenClaw. Written for both humans and AI agents.

---

## Prerequisites

- A **1Password Business or Teams** plan (required for service accounts)
- Admin access to your 1Password account
- Node.js 18+
- OpenClaw installed

## Overview

```
You need three things:
1. A custom vault in 1Password (to store your secrets)
2. A service account (headless access for agents)
3. The plugin installed and configured in OpenClaw
```

---

## Step 1: Create a Custom Vault

Service accounts **cannot** access built-in vaults (Shared, Employee, Private). You must create a custom vault.

### Via 1Password.com (web)

1. Sign in to your 1Password account
2. Click **New Vault** in the sidebar (or **+** button)
3. Name it `Agent Secrets` (or whatever you prefer)
4. Click **Create Vault**

### Via CLI

```bash
op vault create "Agent Secrets" --description "API keys for OpenClaw agents"
```

---

## Step 2: Add Secrets to the Vault

Add the API keys your agents need.

### Via 1Password.com (web)

1. Open the **Agent Secrets** vault
2. Click **New Item** → **API Credential**
3. Title: `OpenAI API`
4. Add a field named `api key` with your OpenAI key as the value
5. Repeat for other secrets (e.g. `Anthropic Auth Token`)

### Via CLI

```bash
op item create --category "API Credential" --title "OpenAI API" \
  --vault "Agent Secrets" 'api key=sk-proj-YOUR_KEY_HERE'

op item create --category "API Credential" --title "Anthropic Auth Token" \
  --vault "Agent Secrets" 'api key=sk-ant-YOUR_TOKEN_HERE'
```

---

## Step 3: Enable Service Accounts (Admin — One Time)

If this is your first service account, an admin must enable the feature.

### Via 1Password.com (web)

1. Sign in as an **owner or admin**
2. Go to **Policies** (in sidebar) → **Developer**
3. Under **Service Accounts**, ensure they're enabled
4. Under **Permissions**, configure which groups can create service accounts

If you see a 403 error when creating a service account, this step was missed.

---

## Step 4: Create a Service Account

**Important:** Service account permissions are **immutable after creation**. You cannot add vault access or change permissions later. If you need different permissions, you must create a new service account.

### Via 1Password.com (web)

1. Sign in to 1Password.com
2. Navigate to **Developer** → **Directory** (in sidebar)
3. Under "Infrastructure Secrets Management", select **Other**
4. Click **Create a Service Account**
5. **Name**: `OpenClaw Agent`
6. **Create vaults**: No (unless you want agents to create vaults)
7. **Vault access**: Select `Agent Secrets`
8. **Click the settings/gear icon** next to `Agent Secrets` to choose permissions:
   - `read_items` — required (lets agents read secrets)
   - `write_items` — optional (lets agents create/update secrets)
9. Click **Create Account**
10. **CRITICAL: Copy the token immediately.** It starts with `ops_` and is shown only once.
11. Click **Save in 1Password** to store a backup of the token

### Via CLI

For read-only access:
```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items"
```

For read + write access:
```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items,write_items"
```

For multiple vaults:
```bash
op service-account create "OpenClaw Agent" \
  --expires-in 0 \
  --vault "Agent Secrets:read_items,write_items" \
  --vault "Production Keys:read_items"
```

The command outputs a token starting with `ops_...`. Save it immediately.

### Changing Permissions

You **cannot** modify an existing service account's permissions. To change them:

1. Create a **new** service account with the desired permissions
2. Save the new token to `~/.openclaw/secrets/op-sa-token`
3. Delete the old service account from **Developer** → **Directory** in 1Password.com

---

## Step 5: Save the Service Account Token

```bash
mkdir -p ~/.openclaw/secrets
```

Write the `ops_...` token into the file (use a text editor, or):
```bash
# Replace ops_YOUR_TOKEN with the actual token
echo "ops_YOUR_TOKEN" > ~/.openclaw/secrets/op-sa-token
chmod 600 ~/.openclaw/secrets/op-sa-token
```

**Verify the file:**
```bash
# Should show the token starts with ops_
head -c 4 ~/.openclaw/secrets/op-sa-token
# Expected output: ops_
```

---

## Step 6: Install the Plugin

### From npm (when published)

```bash
openclaw plugins install @wipcomputer/wip-1password
```

### From local build

```bash
cd /path/to/openclaw-1password
npm install && npm run build
cp -r . ~/.openclaw/extensions/1password/
```

---

## Step 7: Enable in OpenClaw Config

Add to `~/.openclaw/openclaw.json` (merge with existing config):

```json
{
  "plugins": {
    "entries": {
      "1password": {
        "enabled": true,
        "config": {
          "defaultVault": "Agent Secrets"
        }
      }
    }
  }
}
```

---

## Step 8: Test

```bash
# Test 1Password connectivity
openclaw op-secrets test
# Expected: "1Password connection OK — 1 vault(s): Agent Secrets (...)"

# Read a secret (shows redacted preview)
openclaw op-secrets read "OpenAI API"
# Expected: "Agent Secrets/OpenAI API/api key: sk-pro...[154 chars]...EEwA"
```

---

## Step 9: Replace Plaintext Keys with `op://` References

In `openclaw.json`, replace plaintext API keys:

**Before:**
```json
"apiKey": "sk-proj-hTYw...actual-key...EEwA"
```

**After:**
```json
"apiKey": "op://Agent Secrets/OpenAI API/api key"
```

The plugin resolves these at startup. The real key stays in 1Password, never on disk.

### What CAN use `op://` refs

- Any value in `openclaw.json` — resolved at startup by the plugin's resolver service

### What CANNOT use `op://` refs

- `auth-profiles.json` — loaded by a separate OpenClaw subsystem that plugins can't hook into
- Any file outside of `openclaw.json`

---

## Troubleshooting

### "Token file not found"
The file `~/.openclaw/secrets/op-sa-token` doesn't exist or is empty.
```bash
ls -la ~/.openclaw/secrets/op-sa-token
```

### "Token doesn't look like a service account token"
The file exists but doesn't start with `ops_`. It might contain whitespace, a newline, or the wrong token.
```bash
head -c 10 ~/.openclaw/secrets/op-sa-token
```

### "Vault not found"
Service accounts can only access **custom** vaults. Built-in vaults (Shared, Employee, Private) are not supported.
```bash
openclaw op-secrets test  # lists accessible vaults
```

### "403 Forbidden" creating a service account
An admin needs to enable service accounts. See [Step 3](#step-3-enable-service-accounts-admin--one-time).

### "not sufficient permissions for the item update operation"
The service account has `read_items` but not `write_items`. Service account permissions are immutable — you must create a new service account with write access. See [Step 4: Changing Permissions](#changing-permissions).

### "Granting a service account access to the Team/Shared vault is not supported"
You're trying to use a built-in vault. Create a custom vault instead. See [Step 1](#step-1-create-a-custom-vault).

### Plugin not loading
Check that:
1. The plugin is in `~/.openclaw/extensions/` or installed via `openclaw plugins install`
2. `openclaw.json` has `"1password": { "enabled": true }` under `plugins.entries`
3. Run `openclaw doctor` to check for plugin errors

---

## Quick Reference

| Item | Value |
|------|-------|
| Token location | `~/.openclaw/secrets/op-sa-token` |
| Token prefix | `ops_` |
| Config key | `plugins.entries.1password` |
| Default vault | `Agent Secrets` (configurable) |
| Secret ref format | `op://Vault Name/Item Title/field name` |
| Service account permissions | Immutable after creation |

### Agent Tools

| Tool | Purpose | Permissions Needed |
|------|---------|-------------------|
| `op_read_secret` | Read a secret | `read_items` |
| `op_list_items` | List vault items | `read_items` |
| `op_write_secret` | Create/update secret | `read_items` + `write_items` |

### CLI Commands

| Command | Purpose |
|---------|---------|
| `openclaw op-secrets test` | Verify connectivity |
| `openclaw op-secrets read <item>` | Read secret (redacted) |
| `openclaw op-secrets resolve <file>` | Dry-run op:// resolution |


path: mcp-server.mjs
content: #!/usr/bin/env node
// mcp-server.mjs ... MCP server for 1Password secrets
// Uses `op` CLI with service account token for headless access.
// Register with: claude mcp add --scope user wip-1password -- node /path/to/mcp-server.mjs

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { execSync } from "node:child_process";
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";

const HOME = process.env.HOME || "";
const TOKEN_PATH = process.env.OP_SA_TOKEN_PATH || join(HOME, ".openclaw/secrets/op-sa-token");
const DEFAULT_VAULT = process.env.OP_DEFAULT_VAULT || "Agent Secrets";

function getSAToken() {
  if (!existsSync(TOKEN_PATH)) {
    throw new Error(`Service account token not found at ${TOKEN_PATH}`);
  }
  return readFileSync(TOKEN_PATH, "utf8").trim();
}

function opExec(args) {
  const [REDACTED_SECRET]);
  return execSync(`op ${args}`, {
    env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: token },
    encoding: "utf8",
    timeout: 15000,
  }).trim();
}

const server = new Server(
  { name: "wip-1password", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "op_read_secret",
      description: "Read a secret value from 1Password. Returns the decrypted value.",
      inputSchema: {
        type: "object",
        properties: {
          item: { type: "string", description: "Item title (e.g. 'OpenAI API')" },
          vault: { type: "string", description: `Vault name (default: ${DEFAULT_VAULT})` },
          field: { type: "string", description: "Field name (default: 'api key')" },
        },
        required: ["item"],
      },
    },
    {
      name: "op_list_items",
      description: "List all items in a 1Password vault.",
      inputSchema: {
        type: "object",
        properties: {
          vault: { type: "string", description: `Vault name (default: ${DEFAULT_VAULT})` },
        },
      },
    },
    {
      name: "op_test",
      description: "Test 1Password connectivity. Returns account info if successful.",
      inputSchema: { type: "object", properties: {} },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "op_read_secret": {
        const vault = args.vault || DEFAULT_VAULT;
        const field = args.field || "api key";
        const ref = `op://${vault}/${args.item}/${field}`;
        const value = opExec(`read "${ref}"`);
        return {
          content: [{ type: "text", text: value }],
        };
      }

      case "op_list_items": {
        const vault = args.vault || DEFAULT_VAULT;
        const result = opExec(`item list --vault "${vault}" --format json`);
        const items = JSON.parse(result);
        const summary = items.map(i => `- ${i.title} (${i.category})`).join("\n");
        return {
          content: [{ type: "text", text: summary || "No items found." }],
        };
      }

      case "op_test": {
        const result = opExec("whoami --format json");
        const info = JSON.parse(result);
        return {
          content: [{
            type: "text",
            text: `Connected. Account: ${info.account_uuid || info.url || "ok"}`,
          }],
        };
      }

      default:
        return {
          content: [{ type: "text", text: `Unknown tool: ${name}` }],
          isError: true,
        };
    }
  } catch (e) {
    return {
      content: [{ type: "text", text: `Error: ${e.message}` }],
      isError: true,
    };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);


path: openclaw.plugin.json
content: {
  "id": "op-secrets",
  "name": "1Password Secrets",
  "description": "Read secrets from 1Password via JS SDK (headless, service account)",
  "skills": ["./skills"],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "defaultVault": { "type": "string" },
      "tokenPath": { "type": "string" }
    }
  },
  "uiHints": {
    "defaultVault": {
      "label": "Default Vault",
      "placeholder": "Agent Secrets"
    },
    "tokenPath": {
      "label": "Service Account Token Path",
      "placeholder": "~/.openclaw/secrets/op-sa-token",
      "sensitive": true
    }
  }
}


path: package.json
content: {
  "name": "@wipcomputer/wip-1password",
  "version": "0.2.2",
  "description": "OpenClaw plugin for 1Password secrets via JS SDK",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "skills",
    "openclaw.plugin.json"
  ],
  "openclaw": {
    "extensions": [
      "./dist/index.js"
    ]
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "dev": "tsup src/index.ts --format esm --dts --watch",
    "prepublishOnly": "npm run build"
  },
  "keywords": [
    "openclaw",
    "openclaw-plugin",
    "1password",
    "secrets"
  ],
  "license": "MIT",
  "peerDependencies": {
    "openclaw": ">=2026.1.0"
  },
  "dependencies": {
    "@1password/sdk": "^0.3.1",
    "@modelcontextprotocol/sdk": "^1.27.1"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.4.0"
  }
}


path: src/index.ts
content: import { readFileSync, existsSync } from "node:fs";
import {
  createClient,
  type Client,
  ItemCategory,
  ItemFieldType,
} from "@1password/sdk";

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

const PKG_VERSION = "0.1.0";
const DEFAULT_VAULT = "Agent Secrets";
const DEFAULT_TOKEN_PATH = `${process.env.HOME}/.openclaw/secrets/op-sa-token`;

// ── SDK client (lazy singleton) ─────────────────────────────────────

let _client: Client | null = null;
let _tokenPath: string = DEFAULT_TOKEN_PATH;

async function getClient(): Promise<Client> {
  if (_client) return _client;

  const path = process.env.OP_SA_TOKEN_PATH || _tokenPath;
  if (!existsSync(path)) {
    throw new Error(
      `1Password service account token not found at ${path}. ` +
        `See README for setup instructions.`,
    );
  }

  const [REDACTED_SECRET], "utf-8").trim();
  if (!token.startsWith("ops_")) {
    throw new Error(
      `Token at ${path} doesn't look like a 1Password service account token (expected "ops_" prefix).`,
    );
  }

  _client = await createClient({
    auth: token,
    integrationName: "OpenClaw",
    integrationVersion: PKG_VERSION,
  });
  return _client;
}

// ── high-level helpers ──────────────────────────────────────────────

async function resolveSecret(
  vault: string,
  item: string,
  field: string,
): Promise<string> {
  const client = await getClient();
  return await client.secrets.resolve(`op://${vault}/${item}/${field}`);
}

async function listVaults(): Promise<
  Array<{ id: string; title: string }>
> {
  const client = await getClient();
  return await client.vaults.list();
}

async function listItems(
  vaultId: string,
): Promise<Array<{ id: string; title: string; category: string }>> {
  const client = await getClient();
  return await client.items.list(vaultId);
}

function redact(value: string): string {
  if (value.length > 12) {
    return `${value.slice(0, 6)}...[${value.length - 10} chars]...${value.slice(-4)}`;
  }
  return "[short value]";
}

/** MCP-style tool result helper */
function toolResult(text: string, isError = false) {
  return {
    content: [{ type: "text" as const, text }],
    details: undefined as unknown,
    ...(isError ? { isError: true } : {}),
  };
}

// ── resolveSecretRefs ───────────────────────────────────────────────

const OP_REF_PATTERN = /^op:\/\/(.+)\/(.+)\/(.+)$/;

/**
 * Walk a config object and resolve any string values matching `op://vault/item/field`.
 * Returns a new object with secrets resolved. Non-matching values pass through unchanged.
 */
export async function resolveSecretRefs<T>(obj: T): Promise<T> {
  if (typeof obj === "string") {
    const match = obj.match(OP_REF_PATTERN);
    if (match) {
      return (await resolveSecret(match[1], match[2], match[3])) as unknown as T;
    }
    return obj;
  }
  if (Array.isArray(obj)) {
    return (await Promise.all(obj.map((v) => resolveSecretRefs(v)))) as unknown as T;
  }
  if (obj !== null && typeof obj === "object") {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(obj)) {
      result[key] = await resolveSecretRefs(value);
    }
    return result as T;
  }
  return obj;
}

// ── plugin ──────────────────────────────────────────────────────────

const opSecretsPlugin = {
  id: "op-secrets",
  name: "1Password Secrets",
  description:
    "Read secrets from 1Password via JS SDK (headless, no desktop app needed)",
  configSchema: {
    type: "object" as const,
    additionalProperties: false,
    properties: {
      defaultVault: { type: "string" as const },
      tokenPath: { type: "string" as const },
    },
  },

  register(api: OpenClawPluginApi) {
    const pluginConfig = (api.config as any)?.plugins?.entries?.[
      "1password"
    ]?.config;
    const defaultVault = pluginConfig?.defaultVault || DEFAULT_VAULT;
    _tokenPath = pluginConfig?.tokenPath || DEFAULT_TOKEN_PATH;

    // ── Agent tool: read a secret ───────────────────────────────────

    api.registerTool(
      {
        name: "op_read_secret",
        label: "Read 1Password Secret",
        description:
          "Read a secret from 1Password. Returns the secret value for the " +
          "specified vault/item/field. Fully headless via service account. " +
          "NEVER log or echo the returned value.",
        parameters: {
          type: "object",
          properties: {
            vault: {
              type: "string",
              description: `Vault name (default: "${defaultVault}")`,
            },
            item: {
              type: "string",
              description:
                "Item title in the vault (e.g. 'OpenAI API', 'Anthropic Auth Token')",
            },
            field: {
              type: "string",
              description: 'Field name to read (default: "api key")',
            },
          },
          required: ["item"],
        },
        async execute(
          _id: string,
          params: { vault?: string; item: string; field?: string },
        ) {
          const vault = params.vault || defaultVault;
          const field = params.field || "api key";
          try {
            const secret = await resolveSecret(vault, params.item, field);
            if (!secret) {
              return toolResult(
                `No value found for ${vault}/${params.item}/${field}`,
              );
            }
            return toolResult(secret);
          } catch (err: any) {
            return toolResult(`1Password error: ${err.message}`, true);
          }
        },
      } as any,
      { optional: true },
    );

    // ── Agent tool: list items ──────────────────────────────────────

    api.registerTool(
      {
        name: "op_list_items",
        label: "List 1Password Items",
        description:
          "List all items in a 1Password vault. Returns item titles and categories.",
        parameters: {
          type: "object",
          properties: {
            vault: {
              type: "string",
              description: `Vault name (default: "${defaultVault}")`,
            },
          },
        },
        async execute(_id: string, params: { vault?: string }) {
          const vaultName = params.vault || defaultVault;
          try {
            const vaults = await listVaults();
            const vault = vaults.find((v) => v.title === vaultName);
            if (!vault) {
              return toolResult(
                `Vault "${vaultName}" not found. Available: ${vaults.map((v) => v.title).join(", ")}`,
                true,
              );
            }
            const items = await listItems(vault.id);
            const summary = items
              .map((i) => `${i.title} (${i.category})`)
              .join("\n");
            return toolResult(summary || "No items found.");
          } catch (err: any) {
            return toolResult(`1Password error: ${err.message}`, true);
          }
        },
      } as any,
      { optional: true },
    );

    // ── Agent tool: create/update a secret ──────────────────────────

    api.registerTool(
      {
        name: "op_write_secret",
        label: "Write 1Password Secret",
        description:
          "Create or update a secret in 1Password. Use this to store new API keys " +
          "or credentials. The service account must have write access to the vault.",
        parameters: {
          type: "object",
          properties: {
            vault: {
              type: "string",
              description: `Vault name (default: "${defaultVault}")`,
            },
            item: {
              type: "string",
              description: "Item title (e.g. 'New API Key')",
            },
            field: {
              type: "string",
              description: 'Field name (default: "api key")',
            },
            value: {
              type: "string",
              description: "The secret value to store",
            },
            category: {
              type: "string",
              description:
                'Item category (default: "API Credential"). Only used when creating new items.',
            },
          },
          required: ["item", "value"],
        },
        async execute(
          _id: string,
          params: {
            vault?: string;
            item: string;
            field?: string;
            value: string;
            category?: string;
          },
        ) {
          const vaultName = params.vault || defaultVault;
          const field = params.field || "api key";
          try {
            const client = await getClient();
            const vaults = await listVaults();
            const vault = vaults.find((v) => v.title === vaultName);
            if (!vault) {
              return toolResult(`Vault "${vaultName}" not found.`, true);
            }

            // Check if item already exists
            const items = await listItems(vault.id);
            const existing = items.find(
              (i) => i.title.toLowerCase() === params.item.toLowerCase(),
            );

            if (existing) {
              // Update existing item — get it, update field, put it back
              const full = await client.items.get(vault.id, existing.id);
              let fieldFound = false;
              for (const f of full.fields) {
                if (f.title?.toLowerCase() === field.toLowerCase()) {
                  f.value = params.value;
                  fieldFound = true;
                  break;
                }
              }
              if (!fieldFound) {
                full.fields.push({
                  id: "",
                  title: field,
                  value: params.value,
                  fieldType: ItemFieldType.Concealed,
                });
              }
              await client.items.put(full);
              return toolResult(
                `Updated "${params.item}" in ${vaultName} (field: ${field})`,
              );
            } else {
              // Create new item
              await client.items.create({
                vaultId: vault.id,
                title: params.item,
                category: ItemCategory.ApiCredentials,
                fields: [
                  {
                    id: "",
                    title: field,
                    value: params.value,
                    fieldType: ItemFieldType.Concealed,
                  },
                ],
              });
              return toolResult(
                `Created "${params.item}" in ${vaultName} (field: ${field})`,
              );
            }
          } catch (err: any) {
            return toolResult(`1Password error: ${err.message}`, true);
          }
        },
      } as any,
      { optional: true },
    );

    // ── CLI commands ────────────────────────────────────────────────

    api.registerCli(
      ({ program }) => {
        const cmd = program
          .command("op-secrets")
          .description("1Password secrets plugin (SDK)");

        cmd
          .command("test")
          .description("Test 1Password connectivity")
          .action(async () => {
            try {
              console.log("Connecting via service account...");
              const vaults = await listVaults();
              console.log(
                `1Password connection OK — ${vaults.length} vault(s):`,
              );
              for (const v of vaults) {
                const items = await listItems(v.id);
                console.log(
                  `  - ${v.title} (${v.id}) — ${items.length} item(s)`,
                );
                for (const i of items) {
                  console.log(`      ${i.title} (${i.category})`);
                }
              }
            } catch (err: any) {
              console.error("1Password connection FAILED:", err.message);
              process.exit(1);
            }
          });

        cmd
          .command("read")
          .description("Read a secret (redacted preview)")
          .argument("<item>", "Item title")
          .option("-v, --vault <vault>", "Vault name", defaultVault)
          .option("-f, --field <field>", "Field name", "api key")
          .action(
            async (item: string, opts: { vault: string; field: string }) => {
              try {
                const value = await resolveSecret(
                  opts.vault,
                  item,
                  opts.field,
                );
                console.log(
                  `${opts.vault}/${item}/${opts.field}: ${redact(value)}`,
                );
              } catch (err: any) {
                console.error("Read failed:", err.message);
                process.exit(1);
              }
            },
          );

        cmd
          .command("resolve")
          .description("Resolve op:// references in a JSON file (dry run)")
          .argument("<file>", "Path to JSON file")
          .action(async (file: string) => {
            try {
              const raw = readFileSync(file, "utf-8");
              const obj = JSON.parse(raw);
              const resolved = await resolveSecretRefs(obj);
              // Show which keys were resolved (redacted values)
              const diff = findResolvedKeys(obj, resolved);
              if (diff.length === 0) {
                console.log("No op:// references found.");
              } else {
                console.log(`Resolved ${diff.length} secret reference(s):`);
                for (const d of diff) {
                  console.log(`  ${d.path}: ${redact(d.value)}`);
                }
              }
            } catch (err: any) {
              console.error("Resolve failed:", err.message);
              process.exit(1);
            }
          });
      },
      { commands: ["op-secrets"] },
    );

    // ── Service: resolve op:// references at startup ──────────────
    // Config mutation via Object.assign doesn't reliably propagate to all
    // subsystems (memory search reads a separate config snapshot). Instead,
    // resolve known secrets and set them as environment variables, which are
    // globally visible.

    api.registerService?.({
      id: "op-secret-resolver",
      async start(ctx: any) {
        try {
          // Resolve op:// refs in config and mutate in-place (best effort)
          const resolved = await resolveSecretRefs(ctx.config);
          Object.assign(ctx.config, resolved);

          // Resolve OpenAI key directly from 1Password and set as env var.
          // Memory search uses resolveApiKeyForProvider() which checks
          // process.env.OPENAI_API_KEY as a fallback — so we set it here.
          // The config should NOT have remote.apiKey set, because if it does,
          // memory search uses it directly (skipping the env var fallback).
          if (!process.env.OPENAI_API_KEY) {
            try {
              const key = await resolveSecret(defaultVault, "OpenAI API", "api key");
              if (key) {
                process.env.OPENAI_API_KEY = key;
                ctx.logger.info("1password: set OPENAI_API_KEY from 1Password");
              }
            } catch (err: any) {
              ctx.logger.warn(`1password: failed to resolve OpenAI key: ${err.message}`);
            }
          }

          ctx.logger.info("1password: op:// secret references resolved");
        } catch (err: any) {
          ctx.logger.warn(
            `1password: failed to resolve secret references: ${err.message}`
          );
        }
      },
    });

    api.logger.info(
      `1password plugin registered (vault: ${defaultVault}, SDK mode)`,
    );
  },
};

/** Compare original vs resolved config to find which keys changed */
function findResolvedKeys(
  original: any,
  resolved: any,
  path = "",
): Array<{ path: string; value: string }> {
  const results: Array<{ path: string; value: string }> = [];
  if (typeof original === "string" && OP_REF_PATTERN.test(original)) {
    results.push({ path: path || "(root)", value: resolved });
  } else if (original !== null && typeof original === "object") {
    for (const key of Object.keys(original)) {
      results.push(
        ...findResolvedKeys(
          original[key],
          resolved[key],
          path ? `${path}.${key}` : key,
        ),
      );
    }
  }
  return results;
}

export default opSecretsPlugin;


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)