From 84e5b877b235d2020b3062a89fe08101da772838 Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 11:21:08 -0700 Subject: [PATCH 01/10] Add MCP integration and Developer Environments skill to unified plugin Bump the 1password Cursor plugin to v1.2.0 with bundled MCP server config, agent skill, and updated docs while keeping the existing .env validation hook unchanged. --- .cursor-plugin/plugin.json | 14 ++- .gitignore | 4 + README.md | 75 ++++++++++++- assets/icon.svg | 23 ++++ assets/logo-dark.png | Bin 0 -> 4054 bytes assets/logo-light.png | Bin 0 -> 5488 bytes mcp.json | 8 ++ skills/1password-environments/SKILL.md | 143 +++++++++++++++++++++++++ 8 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 assets/icon.svg create mode 100644 assets/logo-dark.png create mode 100644 assets/logo-light.png create mode 100644 mcp.json create mode 100644 skills/1password-environments/SKILL.md diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 0e3e1b2..75d466b 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,11 +1,11 @@ { "name": "1password", - "version": "1.1.0", - "description": "1Password plugin for Cursor — securely manage development secrets.", + "version": "1.2.0", + "description": "1Password plugin for Cursor — validate .env mounts and manage Developer Environments via MCP. Secret values stay in 1Password.", "author": { "name": "1Password" }, - "homepage": "https://1password.com", + "homepage": "https://developer.1password.com/docs/environments", "repository": "https://github.com/1Password/cursor-plugin", "license": "MIT", "keywords": [ @@ -16,8 +16,12 @@ "env", "dotenv", "hooks", - "validation" + "validation", + "mcp", + "developer-environments" ], "logo": "assets/logo.svg", - "hooks": "hooks/hooks.json" + "hooks": "hooks/hooks.json", + "skills": "./skills/", + "mcpServers": "./mcp.json" } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb6b872 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store + +# Local dev hook wiring (use when team policy blocks local plugin imports) +.cursor/hooks.json diff --git a/README.md b/README.md index ffb5555..bf95c48 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 1Password Plugin for Cursor -The official [1Password](https://1password.com) plugin for [Cursor](https://cursor.com). It brings 1Password's secret management capabilities directly into your editor, helping you develop securely without leaving your workflow. +The official [1Password](https://1password.com) plugin for [Cursor](https://cursor.com). It validates locally mounted `.env` files before shell commands run and connects Cursor to the 1Password MCP server to manage [Developer Environments](https://developer.1password.com/docs/environments). Secret values stay in 1Password — the agent sees variable names and mount paths, not secret contents. For more on 1Password's developer tools, see the [1Password Developer Documentation](https://developer.1password.com). @@ -9,7 +9,11 @@ For more on 1Password's developer tools, see the [1Password Developer Documentat - [1Password](https://1password.com) subscription - [1Password for Mac or Linux](https://1password.com/downloads) - [Cursor](https://cursor.com) -- [sqlite3](https://www.sqlite.org/) installed and available in your `PATH` (pre-installed on macOS; install via your package manager on Linux) + +Additional requirements by feature: + +- **Hooks** — [sqlite3](https://www.sqlite.org/) installed and available in your `PATH` (pre-installed on macOS; install via your package manager on Linux) +- **MCP** — 1Password Labs **MCP Server** experiment enabled in the desktop app (`onepassword://settings/labs`). If the setting is missing, your account may not have the `ai-local-mcp-server` feature flag. > **Note:** 1Password Environments local `.env` mounts only apply on **macOS and Linux**. **`hooks.json`** invokes **`./scripts/validate-mounted-env-files`** with no extension. On **macOS / Linux**, that runs the **Bash** script. On **Windows** the shell looks for a real file by trying suffixes from **`PATHEXT`** until one matches on disk. That yields **`validate-mounted-env-files.cmd`**, which returns **`allow`** and skips validation so agent shells are not blocked. @@ -32,6 +36,24 @@ Install from the [Cursor Marketplace](https://cursor.com/marketplace): Or use the command palette: `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) > **Plugins: Install Plugin** > search for `1password`. +Or install directly: + +``` +/add-plugin 1password +``` + +### Step 3: Enable MCP (optional) + +To use Developer Environments MCP tools, enable the **MCP Server** experiment in 1Password: open **Settings → Labs** (or use `onepassword://settings/labs`) and turn on **MCP Server**. + +The MCP server binary on macOS: + +```text +/Applications/1Password.app/Contents/MacOS/1password-mcp +``` + +On Linux, see the [1Password MCP server documentation](https://www.1password.dev/environments/mcp-server) for the binary path on your platform. + ## Features ### Hooks @@ -122,16 +144,50 @@ DEBUG=1 echo '{"command": "echo test", "workspace_roots": ["/path/to/your/projec When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-hooks.log`. Log entries include timestamps and details about 1Password queries, validation results, and permission decisions. +### MCP + +Connect Cursor to the local 1Password MCP server to manage Developer Environments and local `.env` mounts. The bundled skill guides the agent through authentication, environment selection, variable inspection, and mount creation. + +#### Example prompts + +- "List my 1Password Environments" +- "Mount my staging Environment as `.env` in this repo" +- "What variables are in my production Environment?" +- "Create a new Environment called `my-app-dev`" +- "Add a placeholder for my OpenAI API key" + +#### MCP tools + +| Tool | Description | +|------|-------------| +| `authenticate` | Authenticate with the 1Password desktop app; returns `accountId` | +| `list_environments` | List Developer Environments for an account | +| `create_environment` | Create a new Developer Environment | +| `rename_environment` | Rename an existing Developer Environment | +| `list_variables` | List variable names in an Environment (no values) | +| `append_variables` | Add or update Environment variables | +| `create_local_env_file` | Mount an Environment as a local `.env` file | +| `list_local_env_files` | List existing local `.env` mounts for an Environment | + +The server also exposes documentation resources at `1password://docs/getting-started` and `1password://docs/environments-guide`. + +Confirm the MCP server is connected in **Cursor Settings → MCP** after enabling the Labs experiment in 1Password. + ## Plugin Structure ``` -1password/ +cursor-plugin/ ├── .cursor-plugin/ │ └── plugin.json # Plugin manifest ├── hooks/ │ └── hooks.json # Hook event configuration +├── skills/ +│ └── 1password-environments/ +│ └── SKILL.md # Agent skill for MCP workflows +├── mcp.json # MCP server configuration ├── assets/ -│ └── logo.svg # Plugin logo +│ ├── logo.svg # Plugin logo +│ └── icon.svg ├── scripts/ │ ├── validate-mounted-env-files # Bash hook (macOS / Linux) │ └── validate-mounted-env-files.cmd # Windows cmd wrapper returns allow (validation skipped) @@ -139,6 +195,16 @@ When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-h └── README.md ``` +## Local development + +Symlink the repository root for local testing: + +```bash +ln -s /path/to/cursor-plugin ~/.cursor/plugins/local/1password +``` + +Reload Cursor after creating or updating the symlink. + ## Telemetry The plugin emits **opt-in** telemetry so 1Password can understand plugin adoption and the prevalence of common failure modes (missing files, disabled mounts). Two event types are emitted: @@ -159,6 +225,7 @@ The plugin emits **opt-in** telemetry so 1Password can understand plugin adoptio ## Resources - [Validate local `.env` files with Cursor Agent](https://developer.1password.com/docs/environments/cursor-hook-validate/) — full setup guide on the 1Password Developer site +- [1Password MCP server documentation](https://www.1password.dev/environments/mcp-server) - [1Password Agent Hooks](https://github.com/1Password/agent-hooks) — the original hooks repository this plugin is based on - [1Password Environments](https://developer.1password.com/docs/environments) — documentation for 1Password's environment and secrets management - [1Password Local `.env` Files](https://developer.1password.com/docs/environments/local-env-file) — how local `.env` file mounting works diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..eda328f --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/logo-dark.png b/assets/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9f83808a9703edf8c7ee3a8af2e520fcc919de80 GIT binary patch literal 4054 zcmb_fc{r4B_a8C#iHecEj4g~F`=qhNh+*tYmdcQ2ln_IM8ewT~5{qKFQ`#$G$&gXod^E~%o_ldW$GKKMq@j@UF7}gA9 z2Z6B7{Mp>>V8)!+>M7`sT{5$?gg_$XArQ(f2xJFLQNBPRp#TK(6%T>v(jX9#pzH=) zG)TnR;2caq4*-S$3I%AQ0Cg=uT@z5(2Gn%`HE?MIYFd9R7;5PP8vg(Q1!#d~Al3pj zbpB8v25SIa0HnYW&;kH3^AGr=1>XQ-@In0pz)EnP0stLA1NFzMX@lPYZM3NXoifqR z${r$k%n%<$$!nQj=`RCpl8*sV$$>#-D}Z4N-ewxePw5xLm*-P#Qrv^&xV7-0jLp-+ z6a%>fl{5nbo**-q5C{Y=_h++V?POQMBo`5DX~OjtD#XvvQ$9BY9tUg!i#g*!`tUvH zqmg+1VUxMBu}MufSm%ryy^o{MLw#$1Kag-YM(y*UUG2t;!HfOqXEiAfjtyHw3de5= z8gc)R4CcgL{bU8xq4DR%Un57{l9n5aiqpo|3F<6$BON=U@4B~oPR{#|WdYlBRZC0G zg_gihFwtpP`bcBPwa_rx-nP5#rd6n#{@Arnhm7ql^r+L_=l``SILt9A$~J?okR;X6OcDv@u}Rlc+H6 zjc(Ikfh7E;X>ulg^Hs@~(4@=8(O$-gZULbL!3m$%s}{PBpU4k2$$z}{@b0U^3nqo( zJI2|WSvgVm`|(UP`K$=R0_(sw89jeg>XrCsv9>_`jjaI2F|sh?uaZq0)Xk$+^fe11 zE9qmJ1f-rF7xDU_M;Y;#wvxYI>%e-G$f8($3w?uo}p*h@L(IDWI&0OM67a z)zaNp7R&Uz(D{!1Z_LQG?^V;}qDGTH6*1H~4S5DtS*dbRM*k1Ex$^4|mzduSoA#J< z$_zWoufx{S8A|DKt5y66CFixo?xoF0_{z1=ebb@tM7h`HSqV-oC~2g5#mS(ucCcGL zTaW!>JM3P{O`*K};dP@8{<;LIqfv_i#c|%KNX|Wtf{Oa5egYp4*7xI)_B-DiIwxFr z>jz~+l1l?qGaKJco?lKQZtz6>JvKAZvT*5UY9HB5`znMzw){I*cbB8ts&v-fH?U#> ztGh2Xh_3J5&XQnce(H=$EWie=VK=lt<~-VeH+4>r=)_7`*x^Mg!C%X-?x%|9G-rky zV8u(Z2yLFBG8T@l^kujaU$A+{lbQK3=>zk*&ur=Nx#yHGiX~Je^S9373AV_{{p~Bx zPoQN_2_6^Na?-Sv$6Sj!#9oI%-NfHKi(!UAKZ?C6&+-X_W)+9c1jvZOn+_7LN^@Oz zd|!z_eNu(3?651`-5;?1o!8IRWk+&^M*hu4H^~lJ{tcS;FI*+7Q{*T zEVQ7%+X5>Ruos@;hkC?)W_=0LRKG>SH~koCx|Zv_vS1HGpQEa8fJ3AD2>k)!uejs1 zf*kJ8*$+j12ny6wi-cIw4~h6Ll$iU1bK@bL%QEd@u}K_5a^|b8y5bYuPb|`B;zG6S4)uh= z?s@TSAZS+)^|9D*m!*)y6HNdYd0ZvO=4x`k49)*g0OR+7TT40j=dN38``PbH9=&*@ zLao2p`$-WKNEnQWJ-f<*41HiQ&|G1W<3&3Q4A)C0G|wp5$=Ha|9m-B=ewiAZn~26g zQA|{9k9frU8LDC8f>Rqo4DhKX;|moNmD(e`q_)$N(bV`tm82nw-8N(!h^;~aiExA0 z*q$VQuxxHch)YQz)V5rOwn&h37i@maWL4#2sD9+Mbl}d^;BOtac#ff411eecxGzmN z&17jl7PM#97x6AYZ!&SuaI`yn67jhKGmG7 zv$@K5FM%Z{PnXqUY=y-v>ilqTm8E%^(uypkoL^+$fMHK0mZk{XP977+*>%Yrk!aZ) zYjZ5RTO5$WD_`Vj$76NzrR)|I)*v0mlgp8oeN!R4c{Y08F%b)N2C%2$4XjaU zic@3s67I1EU1Cvbpt(FzgU2_DK-({bS*Wd zI|Mcq3`Yp{tZT_q|F$B^^08uB)C*=-MAG4&P>UE0FjUlqp*zey`0Vgln(jW(I}PUS zJ@3nq+Nwunqb!qwNlWuDsfNM^28O{p@k?PIO*2u+16QMXI=>q|@vSU`<~?*HbEodo zaXNblXtF8;X29JEi^|BWxe|W_SGTAC`sZ5M7xLI~^Ho~V_RVAYz4}J$`_c&7^M(p` z*KhE`JvAf1Ep(12)m+Z;L`ft5VvEXcm0~+5U;3l6y0H=kK$)%nxNl+QX> zNQ5+1m~~SyQzhQd6s2BOH|OG&du$vQ;F7=lNvEO+F=vE3g3-7=Fi;cC$j@RONZp*0 z&L0mjQ)DraZG@sm#>A#s5p%LYd_S8L87h*gs9bcT+i@+w2vf_D=r&&#F1Asg9o}IT z#8)2P|6UfZ9#MQWQK8E`ca(7Oq~LIct21jY4Lam6efST4d20x9B;z=)O0+3@LE7NL z>hu?D(kF66cqDBc zC&fE;e$>+Eu+JF@rxwr_jDbadOEuF*L7c9!ecOO4M_M04=vp-&{keu_tHk#+z<2&$ zz%)`27bbQ^J5*7U!y-F&CBRI&U6jMO0T1S7%>mjG)I0#yRs(UUCPn9&Tcj z^cfeCVDL_}N%;INf|K;Dp|q;3QR|JL!~M0o8}L5`OBT$jM`om7&s-t2L;Kl3NJR>W zir{h)S1CT`?NF|2^p0vJa;y!~e$eA1mEBb`O8RfOzZb+fuH28#hU_y`aDn^#KoD`| z-gLIoUZazuaDM`XJ+9mXWP33>1l=5$9 zwVB!F)78%iKRaBOtlJg>t2()_7_&e|bV|MvJehPuR)kY5zR~I&>?QU z)(j+AR~EMSAur#pax^wmaJX;4gc-}ilq|*VX)SGsUw`{>ZEm7h(=BF-t76kVM=~dA zWqVJ`ow-j4CdQ66zpU$yI?7-}29pbF*3CACdm#taA1MgBwa(YMP;{QptS~Bn2)JVV z38LhrWPXy!mPM<>`TS6R2&*|tDgz4*u@fTvZg-*7K!a^EQ-xjM8or*6QJ^zVbZy!w zPJTK`A%B8T)^m`GZC+tIQ#&~PL|(sTQzA#;Qo`y37#H~8`cY+D7q*(M+LP%NRS$tPwRZjvDP@5T&*zIHz{QHhDgy>aohuOeS?lG&m6-@ zHHqD~Nx2)I4^C2Bibwh>Ob6AbVYK;+012O2zs%go#;HjzdjI_;rTo)rX{Qyuku49p z?XDfNEFt}ZZ(J&MR|;r~|8b_t;peVu23JMwiYd1O!TP>~>rI1q_uhs_yqMXZ7=P;%3>Z9)b^jwmRg?5ZvAe&nck@Mqz?Ab-cEpVQ?TXMR$xAjPX*qupN zV@TQwypL~Y)0J3Lf`k*5eB^010>sO2+pRGF|~ zhRF7pSad$V6dE$#VH4nUU6<9>{q<|*Wtrg-kq6V~#OmPA7c6}x(WAruN;RCKNci;A z2UW$z%fE#qE{0MD5e%xpzXbn9AM?X2Di8Im5 zgXpb$DcBoy5Or{3MFLt#b-+PQT^G>Q)dH0N8DAA+KC1ki;A)_kpHKLI7r0DaN&y9m n|KEY=chx(D=y5gZKQw>(ocdQ2X~dH#Py@l5SYcim;i>-z&GRh7 literal 0 HcmV?d00001 diff --git a/assets/logo-light.png b/assets/logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d167b26fe482156ca1f0e97d32b396b93821bed3 GIT binary patch literal 5488 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 L=I4v`NH_lrQ62I1 literal 0 HcmV?d00001 diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..90b8b4e --- /dev/null +++ b/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "1password": { + "command": "/Applications/1Password.app/Contents/MacOS/1password-mcp", + "args": [] + } + } +} diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md new file mode 100644 index 0000000..97637e6 --- /dev/null +++ b/skills/1password-environments/SKILL.md @@ -0,0 +1,143 @@ +--- +name: 1password-environments +description: Use the local 1Password MCP server to work with secure project environment configuration, 1Password Developer Environments, environment variables, API keys, secrets, and local .env mounts. Use when the user asks to set up env vars for a repo, configure secrets, mount or create a local .env from 1Password, store API keys in 1Password, inspect 1Password Environment variable names, or work with the 1Password MCP server. +--- + +# 1Password Environments + +Use the 1Password MCP server for all 1Password Developer Environment work. + +## Use When + +- The user mentions 1Password Environments, 1Password Developer Environments, the 1Password MCP server, or local `.env` files from 1Password. +- The user asks to set up, mount, create, or sync a project `.env` file from a secret manager and 1Password is available. +- The user asks to configure repo environment variables, API keys, tokens, credentials, or secrets securely with 1Password. +- The user wants to list or compare Environment variable names without exposing secret values. + +Do not use this skill for unrelated password-manager tasks, arbitrary local `.env` file parsing, or non-1Password secret stores unless the user asks to migrate that configuration into 1Password. + +## Prerequisites + +- macOS or Linux with the 1Password desktop app installed. +- 1Password Labs MCP server experiment enabled in the desktop app (`onepassword://settings/labs`). +- Access to a 1Password account with Developer Environments enabled. + +The MCP server binary is at: + +```text +/Applications/1Password.app/Contents/MacOS/1password-mcp +``` + +On Linux, see the [1Password MCP server documentation](https://www.1password.dev/environments/mcp-server) for the binary path on your platform. + +If the MCP server is unavailable, direct the user to enable the **1Password Labs MCP Server** experiment in the desktop app. If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. + +## MCP Contents + +The local server exposes these tools: + +- `authenticate` +- `list_environments` +- `create_environment` +- `rename_environment` +- `list_variables` +- `append_variables` +- `create_local_env_file` +- `list_local_env_files` + +The local server also exposes documentation resources: + +- `1password://docs/getting-started` +- `1password://docs/environments-guide` + +No resource templates are currently exposed. + +## Workflow + +1. Call `authenticate` first when you do not already have an account ID for this turn. The 1Password desktop app will ask the user to approve the connection. +2. Use `accountId` for subsequent calls. Server docs may spell the returned value as `account_id`; MCP tool calls use camelCase parameters such as `accountId` and `environmentId`. +3. Call `list_environments` with the returned `accountId` before operating on an Environment, unless the user already provided a current `environmentId`. +4. If the target Environment is ambiguous, ask the user which Environment to use instead of guessing. +5. Use the `environmentId` returned by the server for environment-level calls. +6. Prefer `list_variables` when the user wants to inspect an Environment. It returns names only, not secret values. +7. Use `append_variables` only when the user explicitly asks to add or update variables. +8. Use `create_local_env_file` for local `.env` mounts on macOS or Linux, and pass the absolute `mountPath` the user wants. +9. Use `list_local_env_files` to check existing local mounts before creating a duplicate. + +## Common Flows + +### Authenticate and resolve an Environment + +Most operations start here. Run this sequence at the beginning of any turn unless you already hold both IDs. + +1. Call `authenticate`. The 1Password desktop app will prompt the user for approval on first connection. +2. Store the returned `accountId`. +3. Call `list_environments` with `accountId`. +4. If the target Environment is unambiguous from the user's request, use it. Otherwise, ask the user to choose — do not guess. +5. Store the returned `environmentId`. + +### Mount 1Password as this repo's `.env` + +1. Authenticate and resolve the Environment (see above). +2. Call `list_local_env_files` with `accountId` and `environmentId` to check for an existing mount at the target path. +3. If the user says "here", "this repo", or "this project", derive the absolute path by appending `/.env` to the current workspace root. +4. If no duplicate exists, call `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, and the absolute `mountPath`. +5. Report the mount path and Environment name. Do not read the mounted `.env` file to verify it — use `list_local_env_files` instead. + +### Inspect variables + +1. Authenticate and resolve the Environment (see above). +2. Call `list_variables`. Summarize the returned variable names only — do not request or display values. + +### Create a new Environment + +1. Call `authenticate` and store `accountId`. +2. Confirm the intended name with the user if it was not explicitly stated. +3. Call `create_environment` with `accountId` and the chosen name. +4. Store the `environmentId` from the response for any follow-on operations. +5. If the user wants to add variables immediately, proceed to the "Add or update variables" flow. + +### Rename an Environment + +1. Authenticate and resolve the Environment (see above). +2. Confirm the new name with the user if it was not explicitly stated. +3. Call `rename_environment` with `accountId`, `environmentId`, and the new name. + +### Add or update variables + +1. Confirm the user explicitly wants to create or update variables, and collect any missing names or values before proceeding. +2. Authenticate and resolve the Environment (see above). +3. Call `list_variables` first to identify whether the requested variable names already exist. +4. Call `append_variables` using the active MCP tool schema exactly as exposed in the current session. +5. When the active schema accepts structured variable objects, use `{ "name": "API_KEY", "value": "...", "concealed": true }` for secrets and `concealed: false` only for non-sensitive values such as URLs or feature flags. +6. When the active schema exposes `variables` as `string[]`, do not send unsupported object fields. Use the string format required by that schema, and ask for clarification if the user's requested variable format is ambiguous. + +## Tools + +- `authenticate`: Authenticate with the 1Password desktop app and return the account ID. +- `list_environments`: List Developer Environments for an account. +- `create_environment`: Create a new Developer Environment. +- `rename_environment`: Rename an existing Developer Environment. +- `list_variables`: List variable names in an Environment without returning values. +- `append_variables`: Add or update Environment variables. +- `create_local_env_file`: Mount an Environment as a local `.env` file on macOS or Linux. +- `list_local_env_files`: List local `.env` mounts for an Environment. + +## Error Handling + +- If authentication or environment access fails, tell the user the 1Password desktop app may need approval, unlocking, or account access. +- If the MCP server is unavailable, tell the user to enable the 1Password Labs MCP Server experiment in the desktop app via `onepassword://settings/labs`. +- If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. +- If `create_local_env_file` fails, confirm the user is on macOS or Linux. Local `.env` mounts are documented for macOS and Linux only. + +## Safety + +- Do not reveal, log, or echo secret values. +- Do not read a mounted `.env` file just to verify it exists; use the MCP tools instead. +- Ask before creating or modifying Environment variables unless the user's request is already explicit. +- Treat local `.env` mounts as sensitive even though 1Password does not persist plaintext secret contents to disk. +- If a user pasted a secret into the chat, avoid repeating it back; refer to it by variable name. + +## Notes + +The local MCP server is enabled from 1Password Labs in the desktop app and connects through the bundled `1password-mcp` binary. Local `.env` mounts are supported on macOS and Linux. From 826bdf50d19015b3900d40c0b323e86d32b59adc Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 11:54:25 -0700 Subject: [PATCH 02/10] fixing logo --- .cursor-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 75d466b..cab5f07 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -20,7 +20,7 @@ "mcp", "developer-environments" ], - "logo": "assets/logo.svg", + "logo": "assets/icon.svg", "hooks": "hooks/hooks.json", "skills": "./skills/", "mcpServers": "./mcp.json" From 26f65f8aad31737ac35092c95d822359e7aba5ad Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 13:33:12 -0700 Subject: [PATCH 03/10] Fix .env import hang with shell-parse skill and FIFO read hook Add a plain-text .env import flow that avoids Cursor's Read tool on sensitive paths, and block Read/preToolUse access to 1Password FIFO mounts so mounted .env files cannot stall the agent. --- hooks/hooks.json | 11 ++++ scripts/deny-fifo-env-read | 77 ++++++++++++++++++++++++++ skills/1password-environments/SKILL.md | 29 +++++++++- 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100755 scripts/deny-fifo-env-read diff --git a/hooks/hooks.json b/hooks/hooks.json index d57cfee..def5478 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -5,6 +5,17 @@ { "command": "./scripts/validate-mounted-env-files" } + ], + "preToolUse": [ + { + "matcher": "Read", + "command": "./scripts/deny-fifo-env-read" + } + ], + "beforeReadFile": [ + { + "command": "./scripts/deny-fifo-env-read" + } ] } } diff --git a/scripts/deny-fifo-env-read b/scripts/deny-fifo-env-read new file mode 100755 index 0000000..d52a466 --- /dev/null +++ b/scripts/deny-fifo-env-read @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Block Read/preToolUse access to 1Password FIFO .env mounts. + +Local .env files mounted by 1Password Environments are named pipes. Any read +(cat, Read tool, beforeReadFile content load) blocks until 1Password streams +secrets and appears to hang Cursor. +""" + +from __future__ import annotations + +import json +import os +import stat +import sys + + +DENY_MESSAGE = ( + "This path is a 1Password Environments local .env mount (named pipe/FIFO). " + "Reading it blocks indefinitely. To create or populate a 1Password Environment " + "from env vars, use a plain-text source such as .env.example, ask the user to " + "paste KEY=value lines, or use list_local_env_files and list_variables if the " + "mount already exists." +) + + +def _file_path_from_payload(data: dict) -> str: + path = data.get("file_path") + if isinstance(path, str) and path: + return path + + tool_input = data.get("tool_input") + if isinstance(tool_input, dict): + for key in ("path", "targetFile", "file_path", "filePath"): + value = tool_input.get(key) + if isinstance(value, str) and value: + return value + + return "" + + +def main() -> None: + raw = sys.stdin.read() + if not raw.strip(): + print(json.dumps({"permission": "allow"})) + return + + try: + data = json.loads(raw) + except json.JSONDecodeError: + print(json.dumps({"permission": "allow"})) + return + + file_path = _file_path_from_payload(data) + if not file_path or not os.path.exists(file_path): + print(json.dumps({"permission": "allow"})) + return + + try: + if stat.S_ISFIFO(os.stat(file_path).st_mode): + print( + json.dumps( + { + "permission": "deny", + "user_message": DENY_MESSAGE, + "agent_message": DENY_MESSAGE, + } + ) + ) + return + except OSError: + pass + + print(json.dumps({"permission": "allow"})) + + +if __name__ == "__main__": + main() diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md index 97637e6..18f53b1 100644 --- a/skills/1password-environments/SKILL.md +++ b/skills/1password-environments/SKILL.md @@ -14,7 +14,7 @@ Use the 1Password MCP server for all 1Password Developer Environment work. - The user asks to configure repo environment variables, API keys, tokens, credentials, or secrets securely with 1Password. - The user wants to list or compare Environment variable names without exposing secret values. -Do not use this skill for unrelated password-manager tasks, arbitrary local `.env` file parsing, or non-1Password secret stores unless the user asks to migrate that configuration into 1Password. +Do not use this skill for unrelated password-manager tasks or non-1Password secret stores. Use the **Import from a plain-text `.env` file** flow below when the user asks to migrate an existing on-disk `.env` into 1Password. ## Prerequisites @@ -97,6 +97,27 @@ Most operations start here. Run this sequence at the beginning of any turn unles 4. Store the `environmentId` from the response for any follow-on operations. 5. If the user wants to add variables immediately, proceed to the "Add or update variables" flow. +### Import from a plain-text `.env` file + +Use when the user asks to create a new 1Password Environment from an existing **regular file** on disk (for example `.env` or `.env.local`). There is no MCP "import from file" tool — variables must be parsed locally, then sent with `append_variables`. + +1. Confirm the source path and that the user wants to migrate variables into 1Password. +2. Verify the source is a regular file, not a 1Password mount: + - Run `test -f "$path" && ! test -p "$path"` in the shell. + - If the path is a named pipe (FIFO), stop. That path is a 1Password mount — use `list_local_env_files` and MCP tools instead. Never read a mounted `.env`. +3. **Do not use the Read tool on `.env` paths.** Cursor treats env files as sensitive and the Read tool often blocks or appears to hang (waiting for approval that may not render). Parse the file with a shell command instead: + + ```bash + grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$path" | grep -v '^#' + ``` + + Strip optional surrounding quotes from values in your head before calling MCP; do not paste raw output into chat. +4. Call `authenticate`, then `create_environment` (or resolve an existing Environment if the user named one). +5. Call `append_variables` with the parsed name/value pairs. Pass values to MCP only — do not echo secret values in chat. Mark secrets with `concealed: true`. +6. Optionally offer `create_local_env_file` to replace the plaintext file with a 1Password mount, and suggest removing or gitignoring the old plaintext source. + +If parsing via shell is blocked, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer), or to confirm approval if Cursor shows "Waiting for approval" on a file read. + ### Rename an Environment 1. Authenticate and resolve the Environment (see above). @@ -129,11 +150,13 @@ Most operations start here. Run this sequence at the beginning of any turn unles - If the MCP server is unavailable, tell the user to enable the 1Password Labs MCP Server experiment in the desktop app via `onepassword://settings/labs`. - If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. - If `create_local_env_file` fails, confirm the user is on macOS or Linux. Local `.env` mounts are documented for macOS and Linux only. +- If the agent appears stuck while "reading" a `.env` file, it is usually waiting on Cursor's sensitive-file approval UI (not a 1Password or FIFO issue for plain files). Scroll up in chat for an approval card, send a follow-up message to unblock, or use the shell parsing step above instead of Read. ## Safety -- Do not reveal, log, or echo secret values. -- Do not read a mounted `.env` file just to verify it exists; use the MCP tools instead. +- Do not reveal, log, or echo secret values in chat. +- Do not use the Read tool on `.env` paths — parse plain-text sources via shell when migrating into 1Password. +- Do not read a **mounted** `.env` file (1Password FIFO) for any reason; use MCP tools instead. - Ask before creating or modifying Environment variables unless the user's request is already explicit. - Treat local `.env` mounts as sensitive even though 1Password does not persist plaintext secret contents to disk. - If a user pasted a secret into the chat, avoid repeating it back; refer to it by variable name. From 5da8edfeda9e8f20675320624a32801e6a1983cb Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 13:47:09 -0700 Subject: [PATCH 04/10] fixing script name and scope --- hooks/hooks.json | 4 +- scripts/deny-env-file-read | 93 ++++++++++++++++++++++++++ scripts/deny-fifo-env-read | 77 --------------------- skills/1password-environments/SKILL.md | 2 +- 4 files changed, 96 insertions(+), 80 deletions(-) create mode 100755 scripts/deny-env-file-read delete mode 100755 scripts/deny-fifo-env-read diff --git a/hooks/hooks.json b/hooks/hooks.json index def5478..04d23df 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -9,12 +9,12 @@ "preToolUse": [ { "matcher": "Read", - "command": "./scripts/deny-fifo-env-read" + "command": "./scripts/deny-env-file-read" } ], "beforeReadFile": [ { - "command": "./scripts/deny-fifo-env-read" + "command": "./scripts/deny-env-file-read" } ] } diff --git a/scripts/deny-env-file-read b/scripts/deny-env-file-read new file mode 100755 index 0000000..5b6ef57 --- /dev/null +++ b/scripts/deny-env-file-read @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Block Read/preToolUse access to .env files. + +Cursor often blocks or hangs when the agent uses Read on .env paths — including +plain ASCII files on disk, not only 1Password FIFO mounts. Deny those reads and +steer the agent toward shell parsing or MCP tools instead. +""" + +from __future__ import annotations + +import json +import os +import sys + +# Templates without secrets are OK to read for variable names. +_ENV_READ_ALLOWLIST = frozenset( + { + ".env.example", + ".env.sample", + ".env.template", + ".env.dist", + } +) + +DENY_MESSAGE = ( + "Do not use the Read tool on .env files. Cursor often blocks or hangs on " + ".env reads — including plain text files on disk, not only 1Password mounts. " + "Mounted .env files are also named pipes that block until secrets stream. " + "To import variables into 1Password, parse with a shell command such as " + 'grep -E \'^[A-Za-z_][A-Za-z0-9_]*=\' "$path" | grep -v \'^#\', ask the ' + "user to paste KEY=value lines, or use list_local_env_files and MCP tools " + "for existing 1Password mounts." +) + + +def _file_path_from_payload(data: dict) -> str: + path = data.get("file_path") + if isinstance(path, str) and path: + return path + + tool_input = data.get("tool_input") + if isinstance(tool_input, dict): + for key in ("path", "targetFile", "file_path", "filePath"): + value = tool_input.get(key) + if isinstance(value, str) and value: + return value + + return "" + + +def _is_env_secret_path(path: str) -> bool: + base = os.path.basename(path) + if base == ".env": + return True + if base.startswith(".env.") and base not in _ENV_READ_ALLOWLIST: + return True + return False + + +def main() -> None: + raw = sys.stdin.read() + if not raw.strip(): + print(json.dumps({"permission": "allow"})) + return + + try: + data = json.loads(raw) + except json.JSONDecodeError: + print(json.dumps({"permission": "allow"})) + return + + file_path = _file_path_from_payload(data) + if not file_path or not os.path.exists(file_path): + print(json.dumps({"permission": "allow"})) + return + + if _is_env_secret_path(file_path): + print( + json.dumps( + { + "permission": "deny", + "user_message": DENY_MESSAGE, + "agent_message": DENY_MESSAGE, + } + ) + ) + return + + print(json.dumps({"permission": "allow"})) + + +if __name__ == "__main__": + main() diff --git a/scripts/deny-fifo-env-read b/scripts/deny-fifo-env-read deleted file mode 100755 index d52a466..0000000 --- a/scripts/deny-fifo-env-read +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -"""Block Read/preToolUse access to 1Password FIFO .env mounts. - -Local .env files mounted by 1Password Environments are named pipes. Any read -(cat, Read tool, beforeReadFile content load) blocks until 1Password streams -secrets and appears to hang Cursor. -""" - -from __future__ import annotations - -import json -import os -import stat -import sys - - -DENY_MESSAGE = ( - "This path is a 1Password Environments local .env mount (named pipe/FIFO). " - "Reading it blocks indefinitely. To create or populate a 1Password Environment " - "from env vars, use a plain-text source such as .env.example, ask the user to " - "paste KEY=value lines, or use list_local_env_files and list_variables if the " - "mount already exists." -) - - -def _file_path_from_payload(data: dict) -> str: - path = data.get("file_path") - if isinstance(path, str) and path: - return path - - tool_input = data.get("tool_input") - if isinstance(tool_input, dict): - for key in ("path", "targetFile", "file_path", "filePath"): - value = tool_input.get(key) - if isinstance(value, str) and value: - return value - - return "" - - -def main() -> None: - raw = sys.stdin.read() - if not raw.strip(): - print(json.dumps({"permission": "allow"})) - return - - try: - data = json.loads(raw) - except json.JSONDecodeError: - print(json.dumps({"permission": "allow"})) - return - - file_path = _file_path_from_payload(data) - if not file_path or not os.path.exists(file_path): - print(json.dumps({"permission": "allow"})) - return - - try: - if stat.S_ISFIFO(os.stat(file_path).st_mode): - print( - json.dumps( - { - "permission": "deny", - "user_message": DENY_MESSAGE, - "agent_message": DENY_MESSAGE, - } - ) - ) - return - except OSError: - pass - - print(json.dumps({"permission": "allow"})) - - -if __name__ == "__main__": - main() diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md index 18f53b1..8f19d76 100644 --- a/skills/1password-environments/SKILL.md +++ b/skills/1password-environments/SKILL.md @@ -150,7 +150,7 @@ If parsing via shell is blocked, ask the user to paste `KEY=value` lines (withou - If the MCP server is unavailable, tell the user to enable the 1Password Labs MCP Server experiment in the desktop app via `onepassword://settings/labs`. - If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. - If `create_local_env_file` fails, confirm the user is on macOS or Linux. Local `.env` mounts are documented for macOS and Linux only. -- If the agent appears stuck while "reading" a `.env` file, it is usually waiting on Cursor's sensitive-file approval UI (not a 1Password or FIFO issue for plain files). Scroll up in chat for an approval card, send a follow-up message to unblock, or use the shell parsing step above instead of Read. +- If the agent appears stuck while "reading" a `.env` file, Cursor is likely waiting on sensitive-file approval that never renders. The plugin's Read hooks deny `.env` reads (plain files and FIFO mounts) and redirect to shell parsing — if you still see a hang, scroll up for a hidden approval card or send a follow-up message to unblock. ## Safety From 8299311570745b3b3aa3918111b5737c8e70e398 Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 14:34:34 -0700 Subject: [PATCH 05/10] Match Read File V2 in env deny hook and document mount conflict Extend deny-env-file-read for Read File V2, add failClosed and exit 2 on deny, and document the Read-vs-shell validation deadlock in the import skill. --- hooks/hooks.json | 8 +-- scripts/deny-env-file-read | 68 ++++++++++++++++++++------ skills/1password-environments/SKILL.md | 16 +++++- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index 04d23df..0bde030 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -8,13 +8,15 @@ ], "preToolUse": [ { - "matcher": "Read", - "command": "./scripts/deny-env-file-read" + "command": "./scripts/deny-env-file-read", + "matcher": "Read File V2|Read", + "failClosed": true } ], "beforeReadFile": [ { - "command": "./scripts/deny-env-file-read" + "command": "./scripts/deny-env-file-read", + "failClosed": true } ] } diff --git a/scripts/deny-env-file-read b/scripts/deny-env-file-read index 5b6ef57..27b1a37 100755 --- a/scripts/deny-env-file-read +++ b/scripts/deny-env-file-read @@ -10,6 +10,7 @@ from __future__ import annotations import json import os +import re import sys # Templates without secrets are OK to read for variable names. @@ -22,6 +23,8 @@ _ENV_READ_ALLOWLIST = frozenset( } ) +_READ_TOOL_NAME = re.compile(r"^Read(?: File V2)?$", re.IGNORECASE) + DENY_MESSAGE = ( "Do not use the Read tool on .env files. Cursor often blocks or hangs on " ".env reads — including plain text files on disk, not only 1Password mounts. " @@ -33,6 +36,12 @@ DENY_MESSAGE = ( ) +def _debug(message: str) -> None: + if os.environ.get("DEBUG"): + with open("/tmp/1password-cursor-hooks.log", "a", encoding="utf-8") as log: + log.write(f"deny-env-file-read: {message}\n") + + def _file_path_from_payload(data: dict) -> str: path = data.get("file_path") if isinstance(path, str) and path: @@ -48,6 +57,22 @@ def _file_path_from_payload(data: dict) -> str: return "" +def _is_read_event(data: dict) -> bool: + hook_event = data.get("hook_event_name") + if hook_event == "beforeReadFile": + return True + + tool_name = data.get("tool_name") + if isinstance(tool_name, str) and _READ_TOOL_NAME.match(tool_name.strip()): + return True + + # Some Cursor builds omit tool_name on beforeReadFile-style preToolUse payloads. + if data.get("file_path") and not tool_name: + return True + + return False + + def _is_env_secret_path(path: str) -> bool: base = os.path.basename(path) if base == ".env": @@ -57,36 +82,51 @@ def _is_env_secret_path(path: str) -> bool: return False +def _deny() -> None: + print( + json.dumps( + { + "permission": "deny", + "user_message": DENY_MESSAGE, + "agent_message": DENY_MESSAGE, + } + ) + ) + sys.exit(2) + + +def _allow() -> None: + print(json.dumps({"permission": "allow"})) + + def main() -> None: raw = sys.stdin.read() if not raw.strip(): - print(json.dumps({"permission": "allow"})) + _allow() return try: data = json.loads(raw) except json.JSONDecodeError: - print(json.dumps({"permission": "allow"})) + _allow() + return + + _debug(json.dumps({"event": data.get("hook_event_name"), "tool_name": data.get("tool_name")})) + + if not _is_read_event(data): + _allow() return file_path = _file_path_from_payload(data) if not file_path or not os.path.exists(file_path): - print(json.dumps({"permission": "allow"})) + _allow() return if _is_env_secret_path(file_path): - print( - json.dumps( - { - "permission": "deny", - "user_message": DENY_MESSAGE, - "agent_message": DENY_MESSAGE, - } - ) - ) - return + _debug(f"deny {file_path}") + _deny() - print(json.dumps({"permission": "allow"})) + _allow() if __name__ == "__main__": diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md index 8f19d76..5f63a11 100644 --- a/skills/1password-environments/SKILL.md +++ b/skills/1password-environments/SKILL.md @@ -116,7 +116,19 @@ Use when the user asks to create a new 1Password Environment from an existing ** 5. Call `append_variables` with the parsed name/value pairs. Pass values to MCP only — do not echo secret values in chat. Mark secrets with `concealed: true`. 6. Optionally offer `create_local_env_file` to replace the plaintext file with a 1Password mount, and suggest removing or gitignoring the old plaintext source. -If parsing via shell is blocked, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer), or to confirm approval if Cursor shows "Waiting for approval" on a file read. +**Mount conflict (Read hook vs shell validation hook).** This plugin blocks `.env` reads and recommends shell parsing, but the `beforeShellExecution` hook blocks **all** shell commands when 1Password expects a mount at a path that is missing, disabled, or not a FIFO (for example a plain `.env` still on disk at the mount path). That is a policy deadlock, not a race: Read is denied, then shell is denied too. + +If shell parsing fails with a message about missing, invalid, or disabled environment files: + +1. Check whether 1Password already has a destination for the same path (`list_local_env_files`, or the 1Password app Destinations tab). +2. Run `test -p "$path"` — if false while 1Password lists that path, the file is plain text colliding with an expected mount. +3. Help the user pick a workaround: + - Copy the source to a non-mount path and parse that copy (e.g. `cp .env .env.import` then `grep` `.env.import`). + - Temporarily set `mount_paths = []` in `.1password/environments.toml` to disable mount validation for this repo. + - Fix the mount in 1Password (enable the destination, or remove it until migration finishes). + - Paste `KEY=value` lines into chat instead of parsing from disk. + +If parsing via shell is blocked for other reasons, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer), or to confirm approval if Cursor shows "Waiting for approval" on a file read. ### Rename an Environment @@ -150,7 +162,7 @@ If parsing via shell is blocked, ask the user to paste `KEY=value` lines (withou - If the MCP server is unavailable, tell the user to enable the 1Password Labs MCP Server experiment in the desktop app via `onepassword://settings/labs`. - If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. - If `create_local_env_file` fails, confirm the user is on macOS or Linux. Local `.env` mounts are documented for macOS and Linux only. -- If the agent appears stuck while "reading" a `.env` file, Cursor is likely waiting on sensitive-file approval that never renders. The plugin's Read hooks deny `.env` reads (plain files and FIFO mounts) and redirect to shell parsing — if you still see a hang, scroll up for a hidden approval card or send a follow-up message to unblock. +- If shell commands are denied during a plain `.env` import, see **Mount conflict** under "Import from a plain-text `.env` file" — a plain file at a path where 1Password expects a FIFO mount blocks all shell until the mount is fixed or validation is relaxed. ## Safety From 02d9f19f4dfd8916568f0330cab309503f3988b4 Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 14:49:21 -0700 Subject: [PATCH 06/10] refining skill text and making local file mount an automatic default --- .gitignore | 12 +++ skills/1password-environments/SKILL.md | 115 +++++++++++-------------- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index cb6b872..95ed20e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,16 @@ .DS_Store +# Environment files (keep templates committable) +.env +.env.* +!.env.example +!.env.sample +!.env.template +!.env.dist + +# Python +__pycache__/ +*.py[cod] + # Local dev hook wiring (use when team policy blocks local plugin imports) .cursor/hooks.json diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md index 5f63a11..8726d41 100644 --- a/skills/1password-environments/SKILL.md +++ b/skills/1password-environments/SKILL.md @@ -7,6 +7,8 @@ description: Use the local 1Password MCP server to work with secure project envi Use the 1Password MCP server for all 1Password Developer Environment work. +This skill ships with the **1Password Cursor plugin**, which also runs two hooks: `deny-env-file-read` blocks Read on secret `.env` paths, and `validate-mounted-env-files` blocks shell commands when required local mounts are missing, disabled, or not FIFOs. + ## Use When - The user mentions 1Password Environments, 1Password Developer Environments, the 1Password MCP server, or local `.env` files from 1Password. @@ -16,13 +18,15 @@ Use the 1Password MCP server for all 1Password Developer Environment work. Do not use this skill for unrelated password-manager tasks or non-1Password secret stores. Use the **Import from a plain-text `.env` file** flow below when the user asks to migrate an existing on-disk `.env` into 1Password. -## Prerequisites +## Setup and troubleshooting + +**Requirements** -- macOS or Linux with the 1Password desktop app installed. -- 1Password Labs MCP server experiment enabled in the desktop app (`onepassword://settings/labs`). +- macOS or Linux with the 1Password desktop app installed (local `.env` mounts are macOS/Linux only; on Windows the validation hook is a no-op). +- 1Password Labs **MCP Server** experiment enabled in the desktop app (`onepassword://settings/labs`). - Access to a 1Password account with Developer Environments enabled. -The MCP server binary is at: +The MCP server binary on macOS: ```text /Applications/1Password.app/Contents/MacOS/1password-mcp @@ -30,27 +34,27 @@ The MCP server binary is at: On Linux, see the [1Password MCP server documentation](https://www.1password.dev/environments/mcp-server) for the binary path on your platform. -If the MCP server is unavailable, direct the user to enable the **1Password Labs MCP Server** experiment in the desktop app. If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. - -## MCP Contents +**When things fail** -The local server exposes these tools: +- Authentication or environment access fails — the 1Password desktop app may need approval, unlocking, or account access. +- MCP server unavailable — enable the **1Password Labs MCP Server** experiment via `onepassword://settings/labs`. If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. +- `create_local_env_file` fails — confirm the user is on macOS or Linux. +- Shell commands denied during a plain `.env` import — see **Mount conflict** under "Import from a plain-text `.env` file". -- `authenticate` -- `list_environments` -- `create_environment` -- `rename_environment` -- `list_variables` -- `append_variables` -- `create_local_env_file` -- `list_local_env_files` +## MCP tools and resources -The local server also exposes documentation resources: +| Tool | Description | +|------|-------------| +| `authenticate` | Authenticate with the 1Password desktop app; returns `accountId` | +| `list_environments` | List Developer Environments for an account | +| `create_environment` | Create a new Developer Environment | +| `rename_environment` | Rename an existing Developer Environment | +| `list_variables` | List variable names in an Environment (no values) | +| `append_variables` | Add or update Environment variables | +| `create_local_env_file` | Mount an Environment as a local `.env` file (macOS/Linux) | +| `list_local_env_files` | List existing local `.env` mounts for an Environment | -- `1password://docs/getting-started` -- `1password://docs/environments-guide` - -No resource templates are currently exposed. +Documentation resources: `1password://docs/getting-started`, `1password://docs/environments-guide`. No resource templates are currently exposed. ## Workflow @@ -60,9 +64,8 @@ No resource templates are currently exposed. 4. If the target Environment is ambiguous, ask the user which Environment to use instead of guessing. 5. Use the `environmentId` returned by the server for environment-level calls. 6. Prefer `list_variables` when the user wants to inspect an Environment. It returns names only, not secret values. -7. Use `append_variables` only when the user explicitly asks to add or update variables. -8. Use `create_local_env_file` for local `.env` mounts on macOS or Linux, and pass the absolute `mountPath` the user wants. -9. Use `list_local_env_files` to check existing local mounts before creating a duplicate. +7. Use `append_variables` only when the user explicitly asks to add or update variables, or as part of the **Import from a plain-text `.env` file** flow. +8. Use `create_local_env_file` for local `.env` mounts. When importing from an on-disk `.env`, default `mountPath` to that file's absolute path unless the user explicitly asked not to mount. Full mount steps are in the import flow below. ## Common Flows @@ -79,10 +82,8 @@ Most operations start here. Run this sequence at the beginning of any turn unles ### Mount 1Password as this repo's `.env` 1. Authenticate and resolve the Environment (see above). -2. Call `list_local_env_files` with `accountId` and `environmentId` to check for an existing mount at the target path. -3. If the user says "here", "this repo", or "this project", derive the absolute path by appending `/.env` to the current workspace root. -4. If no duplicate exists, call `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, and the absolute `mountPath`. -5. Report the mount path and Environment name. Do not read the mounted `.env` file to verify it — use `list_local_env_files` instead. +2. If the user says "here", "this repo", or "this project", derive the absolute path by appending `/.env` to the current workspace root. Otherwise use the path they gave. +3. Follow the **Create a local file mount** steps in "Import from a plain-text `.env` file" (step 6), using that path as `mountPath`. ### Inspect variables @@ -99,13 +100,13 @@ Most operations start here. Run this sequence at the beginning of any turn unles ### Import from a plain-text `.env` file -Use when the user asks to create a new 1Password Environment from an existing **regular file** on disk (for example `.env` or `.env.local`). There is no MCP "import from file" tool — variables must be parsed locally, then sent with `append_variables`. +Use when the user asks to create or populate a 1Password Environment from an existing **regular file** on disk (for example `.env` or `.env.local`). There is no MCP "import from file" tool — variables must be parsed locally, then sent with `append_variables`. Unless the user explicitly declines, finish by mounting at the source path (step 6). -1. Confirm the source path and that the user wants to migrate variables into 1Password. +1. Confirm the source path and that the user wants to migrate variables into 1Password. That confirmation covers creating the Environment, appending variables, and mounting unless they opt out of mounting. 2. Verify the source is a regular file, not a 1Password mount: - Run `test -f "$path" && ! test -p "$path"` in the shell. - - If the path is a named pipe (FIFO), stop. That path is a 1Password mount — use `list_local_env_files` and MCP tools instead. Never read a mounted `.env`. -3. **Do not use the Read tool on `.env` paths.** Cursor treats env files as sensitive and the Read tool often blocks or appears to hang (waiting for approval that may not render). Parse the file with a shell command instead: + - If the path is a named pipe (FIFO), stop. That path is a 1Password mount — use `list_local_env_files` and MCP tools instead. +3. **Do not use the Read tool on secret `.env` paths.** This plugin's `deny-env-file-read` hook denies Read on `.env` and `.env.*` files (templates such as `.env.example`, `.env.sample`, `.env.template`, and `.env.dist` are allowed). Parse plaintext sources with a shell command instead: ```bash grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$path" | grep -v '^#' @@ -113,10 +114,21 @@ Use when the user asks to create a new 1Password Environment from an existing ** Strip optional surrounding quotes from values in your head before calling MCP; do not paste raw output into chat. 4. Call `authenticate`, then `create_environment` (or resolve an existing Environment if the user named one). -5. Call `append_variables` with the parsed name/value pairs. Pass values to MCP only — do not echo secret values in chat. Mark secrets with `concealed: true`. -6. Optionally offer `create_local_env_file` to replace the plaintext file with a 1Password mount, and suggest removing or gitignoring the old plaintext source. +5. Call `append_variables` with the parsed name/value pairs. Pass values to MCP only — do not echo secret values in chat. Use `{ "name": "...", "value": "...", "concealed": true }` for secrets and `concealed: false` for non-sensitive values such as URLs or feature flags. +6. **Create a local file mount** at the source file's absolute path (required unless the user explicitly declined): + - Resolve the absolute path to the original `.env` file (for example workspace root + `/.env`). + - Call `list_local_env_files` with `accountId` and `environmentId` to check for an existing mount at that path. + - If no duplicate exists, call `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, and `mountPath` set to that absolute path. + - Report the mount path and Environment name. Verify with `list_local_env_files`, not by reading the mounted file. + - If a temporary copy was used for parsing (see **Mount conflict** below), the mount still targets the original `.env` path; suggest removing or gitignoring the temporary copy after the mount succeeds. + +**Mount conflict (Read hook vs shell validation hook).** The Read hook denies `.env` reads; the `beforeShellExecution` hook blocks **all** shell commands when 1Password expects a mount at a path that is missing, disabled, or not a FIFO (for example a plain `.env` still on disk at the mount path). That is a policy deadlock: Read is denied, then shell is denied too. + +Validation scope depends on `.1password/environments.toml`: -**Mount conflict (Read hook vs shell validation hook).** This plugin blocks `.env` reads and recommends shell parsing, but the `beforeShellExecution` hook blocks **all** shell commands when 1Password expects a mount at a path that is missing, disabled, or not a FIFO (for example a plain `.env` still on disk at the mount path). That is a policy deadlock, not a race: Read is denied, then shell is denied too. +- No TOML file (or no `mount_paths` field) — default mode: all 1Password mount destinations for this workspace are validated. +- `mount_paths = [".env", ...]` — only listed paths are validated. +- `mount_paths = []` — validation disabled for this repo; all shell commands allowed. If shell parsing fails with a message about missing, invalid, or disabled environment files: @@ -128,7 +140,7 @@ If shell parsing fails with a message about missing, invalid, or disabled enviro - Fix the mount in 1Password (enable the destination, or remove it until migration finishes). - Paste `KEY=value` lines into chat instead of parsing from disk. -If parsing via shell is blocked for other reasons, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer), or to confirm approval if Cursor shows "Waiting for approval" on a file read. +If parsing via shell is blocked for other reasons, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer). ### Rename an Environment @@ -141,38 +153,13 @@ If parsing via shell is blocked for other reasons, ask the user to paste `KEY=va 1. Confirm the user explicitly wants to create or update variables, and collect any missing names or values before proceeding. 2. Authenticate and resolve the Environment (see above). 3. Call `list_variables` first to identify whether the requested variable names already exist. -4. Call `append_variables` using the active MCP tool schema exactly as exposed in the current session. -5. When the active schema accepts structured variable objects, use `{ "name": "API_KEY", "value": "...", "concealed": true }` for secrets and `concealed: false` only for non-sensitive values such as URLs or feature flags. -6. When the active schema exposes `variables` as `string[]`, do not send unsupported object fields. Use the string format required by that schema, and ask for clarification if the user's requested variable format is ambiguous. - -## Tools - -- `authenticate`: Authenticate with the 1Password desktop app and return the account ID. -- `list_environments`: List Developer Environments for an account. -- `create_environment`: Create a new Developer Environment. -- `rename_environment`: Rename an existing Developer Environment. -- `list_variables`: List variable names in an Environment without returning values. -- `append_variables`: Add or update Environment variables. -- `create_local_env_file`: Mount an Environment as a local `.env` file on macOS or Linux. -- `list_local_env_files`: List local `.env` mounts for an Environment. - -## Error Handling - -- If authentication or environment access fails, tell the user the 1Password desktop app may need approval, unlocking, or account access. -- If the MCP server is unavailable, tell the user to enable the 1Password Labs MCP Server experiment in the desktop app via `onepassword://settings/labs`. -- If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. -- If `create_local_env_file` fails, confirm the user is on macOS or Linux. Local `.env` mounts are documented for macOS and Linux only. -- If shell commands are denied during a plain `.env` import, see **Mount conflict** under "Import from a plain-text `.env` file" — a plain file at a path where 1Password expects a FIFO mount blocks all shell until the mount is fixed or validation is relaxed. +4. Call `append_variables` with structured objects: `{ "name": "API_KEY", "value": "...", "concealed": true }` for secrets and `concealed: false` for non-sensitive values such as URLs or feature flags. ## Safety - Do not reveal, log, or echo secret values in chat. -- Do not use the Read tool on `.env` paths — parse plain-text sources via shell when migrating into 1Password. +- Do not use the Read tool on secret `.env` paths — parse plaintext sources via shell when migrating into 1Password. Template files (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`) may be read normally. - Do not read a **mounted** `.env` file (1Password FIFO) for any reason; use MCP tools instead. -- Ask before creating or modifying Environment variables unless the user's request is already explicit. +- Ask before creating or modifying Environment variables unless the user's request is already explicit (including "import my `.env`" or "add API_KEY to staging"). - Treat local `.env` mounts as sensitive even though 1Password does not persist plaintext secret contents to disk. - If a user pasted a secret into the chat, avoid repeating it back; refer to it by variable name. - -## Notes - -The local MCP server is enabled from 1Password Labs in the desktop app and connects through the bundled `1password-mcp` binary. Local `.env` mounts are supported on macOS and Linux. From 5581ef26bf78d6964ed3dff919d9b7d1682d509b Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 15:39:03 -0700 Subject: [PATCH 07/10] refining skill --- .cursor-plugin/plugin.json | 4 +- README.md | 25 +-- skills/1password-environments/SKILL.md | 173 +++++++++------------ skills/1password-environments/reference.md | 47 ++++++ 4 files changed, 135 insertions(+), 114 deletions(-) create mode 100644 skills/1password-environments/reference.md diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index cab5f07..ce5f3c3 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "1password", - "version": "1.2.0", - "description": "1Password plugin for Cursor — validate .env mounts and manage Developer Environments via MCP. Secret values stay in 1Password.", + "version": "1.2.1", + "description": "1Password plugin for Cursor — hooks, bundled agent skill, and MCP config for Developer Environments and local .env mounts. Secret values stay in 1Password.", "author": { "name": "1Password" }, diff --git a/README.md b/README.md index bf95c48..544aaad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # 1Password Plugin for Cursor -The official [1Password](https://1password.com) plugin for [Cursor](https://cursor.com). It validates locally mounted `.env` files before shell commands run and connects Cursor to the 1Password MCP server to manage [Developer Environments](https://developer.1password.com/docs/environments). Secret values stay in 1Password — the agent sees variable names and mount paths, not secret contents. +The official [1Password](https://1password.com) plugin for [Cursor](https://cursor.com). It ships three pieces that work together: **hooks** that validate locally mounted `.env` files, an **agent skill** with the complete Developer Environment workflow, and **MCP configuration** for the 1Password desktop app server. Secret values stay in 1Password — the agent sees variable names and mount paths, not secret contents. + +Install the **plugin** (not a hand-configured MCP entry alone). The bundled `1password-environments` skill is the authoritative agent workflow; the MCP server's built-in documentation resources cover tool basics only and omit import-and-mount steps. For more on 1Password's developer tools, see the [1Password Developer Documentation](https://developer.1password.com). @@ -28,7 +30,7 @@ Before using this plugin, you'll need to configure your secrets in 1Password: ### Step 2: Install the plugin -Install from the [Cursor Marketplace](https://cursor.com/marketplace): +Install from the [Cursor Marketplace](https://cursor.com/marketplace). This registers hooks, the `1password-environments` agent skill, and `mcp.json` together. Do not add the 1Password MCP server manually in user settings instead of installing the plugin — agents will get MCP tools without the skill workflow. 1. Open **Cursor Settings** > **Plugins**. 2. Search for **1password**. @@ -42,9 +44,9 @@ Or install directly: /add-plugin 1password ``` -### Step 3: Enable MCP (optional) +### Step 3: Enable MCP in 1Password (required for Environment management) -To use Developer Environments MCP tools, enable the **MCP Server** experiment in 1Password: open **Settings → Labs** (or use `onepassword://settings/labs`) and turn on **MCP Server**. +Enable the **MCP Server** experiment in the 1Password desktop app: open **Settings → Labs** (or use `onepassword://settings/labs`) and turn on **MCP Server**. The plugin's `mcp.json` connects Cursor to that server after this step. The MCP server binary on macOS: @@ -144,9 +146,11 @@ DEBUG=1 echo '{"command": "echo test", "workspace_roots": ["/path/to/your/projec When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-hooks.log`. Log entries include timestamps and details about 1Password queries, validation results, and permission decisions. -### MCP +### MCP and agent skill + +The plugin connects Cursor to the local 1Password MCP server and bundles the **`1password-environments`** skill (`skills/1password-environments/SKILL.md`). Agents MUST read that skill before calling MCP tools. -Connect Cursor to the local 1Password MCP server to manage Developer Environments and local `.env` mounts. The bundled skill guides the agent through authentication, environment selection, variable inspection, and mount creation. +The MCP server exposes `1password://docs/getting-started` and `1password://docs/environments-guide`, but those resources are **not** sufficient for agent workflows — they omit importing a plain `.env` file and mounting at the source path by default. The bundled skill defines the complete workflow, including import, append, and mount. #### Example prompts @@ -154,6 +158,8 @@ Connect Cursor to the local 1Password MCP server to manage Developer Environment - "Mount my staging Environment as `.env` in this repo" - "What variables are in my production Environment?" - "Create a new Environment called `my-app-dev`" +- "Create an Environment from my project `.env` file" +- "Import `.env` into 1Password and mount it here" - "Add a placeholder for my OpenAI API key" #### MCP tools @@ -169,9 +175,7 @@ Connect Cursor to the local 1Password MCP server to manage Developer Environment | `create_local_env_file` | Mount an Environment as a local `.env` file | | `list_local_env_files` | List existing local `.env` mounts for an Environment | -The server also exposes documentation resources at `1password://docs/getting-started` and `1password://docs/environments-guide`. - -Confirm the MCP server is connected in **Cursor Settings → MCP** after enabling the Labs experiment in 1Password. +Confirm the MCP server is connected in **Cursor Settings → MCP** after installing the plugin and enabling the Labs experiment in 1Password. ## Plugin Structure @@ -183,7 +187,8 @@ cursor-plugin/ │ └── hooks.json # Hook event configuration ├── skills/ │ └── 1password-environments/ -│ └── SKILL.md # Agent skill for MCP workflows +│ ├── SKILL.md # Agent skill for MCP workflows +│ └── reference.md # Mount conflict and troubleshooting ├── mcp.json # MCP server configuration ├── assets/ │ ├── logo.svg # Plugin logo diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md index 8726d41..aff0f25 100644 --- a/skills/1password-environments/SKILL.md +++ b/skills/1password-environments/SKILL.md @@ -1,47 +1,40 @@ --- name: 1password-environments -description: Use the local 1Password MCP server to work with secure project environment configuration, 1Password Developer Environments, environment variables, API keys, secrets, and local .env mounts. Use when the user asks to set up env vars for a repo, configure secrets, mount or create a local .env from 1Password, store API keys in 1Password, inspect 1Password Environment variable names, or work with the 1Password MCP server. +description: >- + Provides the canonical Cursor agent workflow for 1Password Developer Environments + via MCP — supersedes the MCP server's getting-started and environments-guide + resources, which omit import and mount steps. Use when the user asks to create, + import, migrate, or mount plain .env files with 1Password, configure repo secrets, + list Environment variable names, manage Developer Environments, or call any + 1Password MCP tool. Requires reading this skill in full before the first MCP call + in a turn. --- # 1Password Environments -Use the 1Password MCP server for all 1Password Developer Environment work. +**MUST:** Read this entire skill before calling any 1Password MCP tool. -This skill ships with the **1Password Cursor plugin**, which also runs two hooks: `deny-env-file-read` blocks Read on secret `.env` paths, and `validate-mounted-env-files` blocks shell commands when required local mounts are missing, disabled, or not FIFOs. +This skill ships with the **1Password Cursor plugin** (hooks + MCP config + this skill). It is the **authoritative agent workflow** for Developer Environments in Cursor. -## Use When +## Quick start -- The user mentions 1Password Environments, 1Password Developer Environments, the 1Password MCP server, or local `.env` files from 1Password. -- The user asks to set up, mount, create, or sync a project `.env` file from a secret manager and 1Password is available. -- The user asks to configure repo environment variables, API keys, tokens, credentials, or secrets securely with 1Password. -- The user wants to list or compare Environment variable names without exposing secret values. +1. Read this skill (required before MCP calls). +2. **Route by intent:** + - On-disk `.env` to import, migrate, or populate from → [Import from a plain-text `.env` file](#import-from-a-plain-text-env-file) (through mount). + - Mount an existing Environment → [Mount 1Password as this repo's `.env`](#mount-1password-as-this-repos-env). + - New empty Environment (name only) → [Create a new Environment](#create-a-new-environment). + - Add or update specific variables → [Add or update variables](#add-or-update-variables). + - Inspect names only → [Inspect variables](#inspect-variables). +3. Call `authenticate`, then proceed with the chosen flow. -Do not use this skill for unrelated password-manager tasks or non-1Password secret stores. Use the **Import from a plain-text `.env` file** flow below when the user asks to migrate an existing on-disk `.env` into 1Password. +## Gotchas -## Setup and troubleshooting +- **MCP built-in docs are incomplete.** The server may point to `1password://docs/getting-started` and `1password://docs/environments-guide`. Those cover tool basics only — not import or default mount. Follow **this skill**, not those resources alone. +- **Install the plugin, not MCP alone.** Manual MCP setup without the plugin leaves agents without this workflow. +- **"Create from `.env`" is an import.** Do not stop after `create_environment` and `append_variables`; finish with `create_local_env_file` unless the user declines. +- **Do not Read secret `.env` paths.** Parse with shell `grep`. Templates (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`) may be read normally. -**Requirements** - -- macOS or Linux with the 1Password desktop app installed (local `.env` mounts are macOS/Linux only; on Windows the validation hook is a no-op). -- 1Password Labs **MCP Server** experiment enabled in the desktop app (`onepassword://settings/labs`). -- Access to a 1Password account with Developer Environments enabled. - -The MCP server binary on macOS: - -```text -/Applications/1Password.app/Contents/MacOS/1password-mcp -``` - -On Linux, see the [1Password MCP server documentation](https://www.1password.dev/environments/mcp-server) for the binary path on your platform. - -**When things fail** - -- Authentication or environment access fails — the 1Password desktop app may need approval, unlocking, or account access. -- MCP server unavailable — enable the **1Password Labs MCP Server** experiment via `onepassword://settings/labs`. If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. -- `create_local_env_file` fails — confirm the user is on macOS or Linux. -- Shell commands denied during a plain `.env` import — see **Mount conflict** under "Import from a plain-text `.env` file". - -## MCP tools and resources +## MCP tools | Tool | Description | |------|-------------| @@ -54,112 +47,88 @@ On Linux, see the [1Password MCP server documentation](https://www.1password.dev | `create_local_env_file` | Mount an Environment as a local `.env` file (macOS/Linux) | | `list_local_env_files` | List existing local `.env` mounts for an Environment | -Documentation resources: `1password://docs/getting-started`, `1password://docs/environments-guide`. No resource templates are currently exposed. - -## Workflow +MCP tool parameters use camelCase (`accountId`, `environmentId`). Server responses may use snake_case (`account_id`). -1. Call `authenticate` first when you do not already have an account ID for this turn. The 1Password desktop app will ask the user to approve the connection. -2. Use `accountId` for subsequent calls. Server docs may spell the returned value as `account_id`; MCP tool calls use camelCase parameters such as `accountId` and `environmentId`. -3. Call `list_environments` with the returned `accountId` before operating on an Environment, unless the user already provided a current `environmentId`. -4. If the target Environment is ambiguous, ask the user which Environment to use instead of guessing. -5. Use the `environmentId` returned by the server for environment-level calls. -6. Prefer `list_variables` when the user wants to inspect an Environment. It returns names only, not secret values. -7. Use `append_variables` only when the user explicitly asks to add or update variables, or as part of the **Import from a plain-text `.env` file** flow. -8. Use `create_local_env_file` for local `.env` mounts. When importing from an on-disk `.env`, default `mountPath` to that file's absolute path unless the user explicitly asked not to mount. Full mount steps are in the import flow below. - -## Common Flows +## Common flows ### Authenticate and resolve an Environment -Most operations start here. Run this sequence at the beginning of any turn unless you already hold both IDs. +Run at the start of any turn unless you already hold both IDs. -1. Call `authenticate`. The 1Password desktop app will prompt the user for approval on first connection. -2. Store the returned `accountId`. -3. Call `list_environments` with `accountId`. -4. If the target Environment is unambiguous from the user's request, use it. Otherwise, ask the user to choose — do not guess. -5. Store the returned `environmentId`. +1. Call `authenticate` (desktop app prompts for approval on first connection). +2. Store `accountId`. Call `list_environments` unless the user provided a current `environmentId`. +3. Pick the target Environment from the user's request; ask if ambiguous — do not guess. +4. Store `environmentId`. ### Mount 1Password as this repo's `.env` -1. Authenticate and resolve the Environment (see above). -2. If the user says "here", "this repo", or "this project", derive the absolute path by appending `/.env` to the current workspace root. Otherwise use the path they gave. -3. Follow the **Create a local file mount** steps in "Import from a plain-text `.env` file" (step 6), using that path as `mountPath`. +1. Authenticate and resolve the Environment. +2. Path: workspace root + `/.env` when the user says "here", "this repo", or "this project"; otherwise use their path. +3. Follow step 6 of [Import from a plain-text `.env` file](#import-from-a-plain-text-env-file). ### Inspect variables -1. Authenticate and resolve the Environment (see above). -2. Call `list_variables`. Summarize the returned variable names only — do not request or display values. +1. Authenticate and resolve the Environment. +2. Call `list_variables`. Summarize names only — never request or display values. ### Create a new Environment +For a **new empty Environment** only (name, no `.env` import). + 1. Call `authenticate` and store `accountId`. -2. Confirm the intended name with the user if it was not explicitly stated. +2. Confirm the name if not explicitly stated. 3. Call `create_environment` with `accountId` and the chosen name. -4. Store the `environmentId` from the response for any follow-on operations. -5. If the user wants to add variables immediately, proceed to the "Add or update variables" flow. +4. Store `environmentId`. If variables come from a file, switch to the import flow. ### Import from a plain-text `.env` file -Use when the user asks to create or populate a 1Password Environment from an existing **regular file** on disk (for example `.env` or `.env.local`). There is no MCP "import from file" tool — variables must be parsed locally, then sent with `append_variables`. Unless the user explicitly declines, finish by mounting at the source path (step 6). +For creating or populating an Environment from a **regular file** on disk (`.env`, `.env.local`, or "values from the project `.env`"). No MCP import tool — parse locally, then `append_variables`. **Mount at the source path by default** (step 6). -1. Confirm the source path and that the user wants to migrate variables into 1Password. That confirmation covers creating the Environment, appending variables, and mounting unless they opt out of mounting. -2. Verify the source is a regular file, not a 1Password mount: - - Run `test -f "$path" && ! test -p "$path"` in the shell. - - If the path is a named pipe (FIFO), stop. That path is a 1Password mount — use `list_local_env_files` and MCP tools instead. -3. **Do not use the Read tool on secret `.env` paths.** This plugin's `deny-env-file-read` hook denies Read on `.env` and `.env.*` files (templates such as `.env.example`, `.env.sample`, `.env.template`, and `.env.dist` are allowed). Parse plaintext sources with a shell command instead: +1. Confirm source path; migration includes create, append, and mount unless the user opts out. +2. Verify plain file: `test -f "$path" && ! test -p "$path"`. If FIFO, stop — use `list_local_env_files` instead. +3. Parse (do not Read secret paths): ```bash grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$path" | grep -v '^#' ``` - Strip optional surrounding quotes from values in your head before calling MCP; do not paste raw output into chat. -4. Call `authenticate`, then `create_environment` (or resolve an existing Environment if the user named one). -5. Call `append_variables` with the parsed name/value pairs. Pass values to MCP only — do not echo secret values in chat. Use `{ "name": "...", "value": "...", "concealed": true }` for secrets and `concealed: false` for non-sensitive values such as URLs or feature flags. -6. **Create a local file mount** at the source file's absolute path (required unless the user explicitly declined): - - Resolve the absolute path to the original `.env` file (for example workspace root + `/.env`). - - Call `list_local_env_files` with `accountId` and `environmentId` to check for an existing mount at that path. - - If no duplicate exists, call `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, and `mountPath` set to that absolute path. - - Report the mount path and Environment name. Verify with `list_local_env_files`, not by reading the mounted file. - - If a temporary copy was used for parsing (see **Mount conflict** below), the mount still targets the original `.env` path; suggest removing or gitignoring the temporary copy after the mount succeeds. - -**Mount conflict (Read hook vs shell validation hook).** The Read hook denies `.env` reads; the `beforeShellExecution` hook blocks **all** shell commands when 1Password expects a mount at a path that is missing, disabled, or not a FIFO (for example a plain `.env` still on disk at the mount path). That is a policy deadlock: Read is denied, then shell is denied too. - -Validation scope depends on `.1password/environments.toml`: + Strip optional quotes before MCP; do not paste raw output into chat. +4. `authenticate`, then `create_environment` or resolve an existing Environment. +5. `append_variables` with `{ "name", "value", "concealed" }` — `true` for secrets, `false` for non-sensitive config. Pass values to MCP only. +6. **Mount** at the source absolute path (required unless declined): + - `list_local_env_files` — skip if mount already exists at path. + - `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, `mountPath`. + - Verify with `list_local_env_files`, not by reading the mounted file. -- No TOML file (or no `mount_paths` field) — default mode: all 1Password mount destinations for this workspace are validated. -- `mount_paths = [".env", ...]` — only listed paths are validated. -- `mount_paths = []` — validation disabled for this repo; all shell commands allowed. +**Import completion checklist:** -If shell parsing fails with a message about missing, invalid, or disabled environment files: +- [ ] Environment created or resolved +- [ ] Variables appended +- [ ] Mount created at source path (unless user declined) +- [ ] Mount verified with `list_local_env_files` -1. Check whether 1Password already has a destination for the same path (`list_local_env_files`, or the 1Password app Destinations tab). -2. Run `test -p "$path"` — if false while 1Password lists that path, the file is plain text colliding with an expected mount. -3. Help the user pick a workaround: - - Copy the source to a non-mount path and parse that copy (e.g. `cp .env .env.import` then `grep` `.env.import`). - - Temporarily set `mount_paths = []` in `.1password/environments.toml` to disable mount validation for this repo. - - Fix the mount in 1Password (enable the destination, or remove it until migration finishes). - - Paste `KEY=value` lines into chat instead of parsing from disk. - -If parsing via shell is blocked for other reasons, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer). +If shell parsing is blocked, see [reference.md](reference.md) (mount conflict and troubleshooting). ### Rename an Environment -1. Authenticate and resolve the Environment (see above). -2. Confirm the new name with the user if it was not explicitly stated. +1. Authenticate and resolve the Environment. +2. Confirm the new name if not stated. 3. Call `rename_environment` with `accountId`, `environmentId`, and the new name. ### Add or update variables -1. Confirm the user explicitly wants to create or update variables, and collect any missing names or values before proceeding. -2. Authenticate and resolve the Environment (see above). -3. Call `list_variables` first to identify whether the requested variable names already exist. -4. Call `append_variables` with structured objects: `{ "name": "API_KEY", "value": "...", "concealed": true }` for secrets and `concealed: false` for non-sensitive values such as URLs or feature flags. +1. Confirm the user wants to create or update variables; collect missing names/values. +2. Authenticate and resolve the Environment. +3. `list_variables` to check existing names. +4. `append_variables` with `{ "name", "value", "concealed" }`. ## Safety - Do not reveal, log, or echo secret values in chat. -- Do not use the Read tool on secret `.env` paths — parse plaintext sources via shell when migrating into 1Password. Template files (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`) may be read normally. -- Do not read a **mounted** `.env` file (1Password FIFO) for any reason; use MCP tools instead. -- Ask before creating or modifying Environment variables unless the user's request is already explicit (including "import my `.env`" or "add API_KEY to staging"). -- Treat local `.env` mounts as sensitive even though 1Password does not persist plaintext secret contents to disk. -- If a user pasted a secret into the chat, avoid repeating it back; refer to it by variable name. +- Do not Read secret `.env` paths or mounted FIFO `.env` files; use MCP tools for mounted files. +- Ask before creating or modifying variables unless the request is explicit ("import my `.env`", "add API_KEY to staging"). +- If the user pasted a secret, refer to it by variable name only. + +## Additional resources + +- Mount conflict, validation modes, and setup troubleshooting: [reference.md](reference.md) diff --git a/skills/1password-environments/reference.md b/skills/1password-environments/reference.md new file mode 100644 index 0000000..87a2f76 --- /dev/null +++ b/skills/1password-environments/reference.md @@ -0,0 +1,47 @@ +# 1Password Environments — reference + +## Mount conflict (Read hook vs shell validation hook) + +The Read hook denies `.env` reads; the `beforeShellExecution` hook blocks **all** shell commands when 1Password expects a mount at a path that is missing, disabled, or not a FIFO (for example a plain `.env` still on disk at the mount path). That is a policy deadlock: Read is denied, then shell is denied too. + +Validation scope depends on `.1password/environments.toml`: + +- No TOML file (or no `mount_paths` field) — default mode: all 1Password mount destinations for this workspace are validated. +- `mount_paths = [".env", ...]` — only listed paths are validated. +- `mount_paths = []` — validation disabled for this repo; all shell commands allowed. + +If shell parsing fails with a message about missing, invalid, or disabled environment files: + +1. Check whether 1Password already has a destination for the same path (`list_local_env_files`, or the 1Password app Destinations tab). +2. Run `test -p "$path"` — if false while 1Password lists that path, the file is plain text colliding with an expected mount. +3. Help the user pick a workaround: + - Copy the source to a non-mount path and parse that copy (e.g. `cp .env .env.import` then `grep` `.env.import`). + - Temporarily set `mount_paths = []` in `.1password/environments.toml` to disable mount validation for this repo. + - Fix the mount in 1Password (enable the destination, or remove it until migration finishes). + - Paste `KEY=value` lines into chat instead of parsing from disk. + +If parsing via shell is blocked for other reasons, ask the user to paste `KEY=value` lines (without values in follow-up messages if they prefer). + +## Setup and troubleshooting + +**Requirements** + +- macOS or Linux with the 1Password desktop app installed (local `.env` mounts are macOS/Linux only; on Windows the validation hook is a no-op). +- **1Password Cursor plugin installed** (marketplace or local symlink) so this skill, hooks, and MCP config load together. +- 1Password Labs **MCP Server** experiment enabled in the desktop app (`onepassword://settings/labs`). +- Access to a 1Password account with Developer Environments enabled. + +The MCP server binary on macOS: + +```text +/Applications/1Password.app/Contents/MacOS/1password-mcp +``` + +On Linux, see the [1Password MCP server documentation](https://www.1password.dev/environments/mcp-server) for the binary path on your platform. + +**When things fail** + +- Authentication or environment access fails — the 1Password desktop app may need approval, unlocking, or account access. +- MCP server unavailable — enable the **1Password Labs MCP Server** experiment via `onepassword://settings/labs`. If the Labs setting is missing, the account may not have the required `ai-local-mcp-server` feature flag. +- `create_local_env_file` fails — confirm the user is on macOS or Linux. +- Shell commands denied during a plain `.env` import — see **Mount conflict** above. From 1245e5fca67f46e286eb4c481f6e2b5445f96550 Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 16:02:21 -0700 Subject: [PATCH 08/10] adding additional rules, instructions, and guard rails --- .cursor-plugin/plugin.json | 2 +- README.md | 12 +- hooks/hooks.json | 18 ++ rules/1password-environments.mdc | 34 +++ scripts/deny-env-file-read | 12 +- scripts/nudge-1password-import | 236 +++++++++++++++++++++ skills/1password-environments/SKILL.md | 151 +++++-------- skills/1password-environments/reference.md | 4 + 8 files changed, 359 insertions(+), 110 deletions(-) create mode 100644 rules/1password-environments.mdc create mode 100755 scripts/nudge-1password-import diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index ce5f3c3..72d8475 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "1password", - "version": "1.2.1", + "version": "1.3.0", "description": "1Password plugin for Cursor — hooks, bundled agent skill, and MCP config for Developer Environments and local .env mounts. Secret values stay in 1Password.", "author": { "name": "1Password" diff --git a/README.md b/README.md index 544aaad..5b7a88d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The official [1Password](https://1password.com) plugin for [Cursor](https://cursor.com). It ships three pieces that work together: **hooks** that validate locally mounted `.env` files, an **agent skill** with the complete Developer Environment workflow, and **MCP configuration** for the 1Password desktop app server. Secret values stay in 1Password — the agent sees variable names and mount paths, not secret contents. -Install the **plugin** (not a hand-configured MCP entry alone). The bundled `1password-environments` skill is the authoritative agent workflow; the MCP server's built-in documentation resources cover tool basics only and omit import-and-mount steps. +Install the **plugin** (not a hand-configured MCP entry alone). The bundled `1password-environments` skill and rule are the authoritative agent workflow; the MCP server's built-in documentation resources cover tool basics only and omit import-and-mount steps. For more on 1Password's developer tools, see the [1Password Developer Documentation](https://developer.1password.com). @@ -148,7 +148,9 @@ When not running in debug mode, the hook writes logs to `/tmp/1password-cursor-h ### MCP and agent skill -The plugin connects Cursor to the local 1Password MCP server and bundles the **`1password-environments`** skill (`skills/1password-environments/SKILL.md`). Agents MUST read that skill before calling MCP tools. +The plugin connects Cursor to the local 1Password MCP server and bundles the **`1password-environments`** skill and **rule** (`skills/1password-environments/SKILL.md`, `rules/1password-environments.mdc`). Agents MUST read that skill before calling MCP tools. + +A **`stop` / `afterMCPExecution` hook** (`scripts/nudge-1password-import`) injects the remaining import steps when an agent stops after `append_variables` without mounting — the common failure mode for `.env` imports. The MCP server exposes `1password://docs/getting-started` and `1password://docs/environments-guide`, but those resources are **not** sufficient for agent workflows — they omit importing a plain `.env` file and mounting at the source path by default. The bundled skill defines the complete workflow, including import, append, and mount. @@ -189,13 +191,17 @@ cursor-plugin/ │ └── 1password-environments/ │ ├── SKILL.md # Agent skill for MCP workflows │ └── reference.md # Mount conflict and troubleshooting +├── rules/ +│ └── 1password-environments.mdc # Agent rule (activates on 1Password MCP work) ├── mcp.json # MCP server configuration ├── assets/ │ ├── logo.svg # Plugin logo │ └── icon.svg ├── scripts/ │ ├── validate-mounted-env-files # Bash hook (macOS / Linux) -│ └── validate-mounted-env-files.cmd # Windows cmd wrapper returns allow (validation skipped) +│ ├── validate-mounted-env-files.cmd # Windows cmd wrapper returns allow (validation skipped) +│ ├── deny-env-file-read # Blocks Read on secret .env paths +│ └── nudge-1password-import # Nudges agents to finish .env import + mount ├── LICENSE └── README.md ``` diff --git a/hooks/hooks.json b/hooks/hooks.json index 0bde030..b09dbb2 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -18,6 +18,24 @@ "command": "./scripts/deny-env-file-read", "failClosed": true } + ], + "afterMCPExecution": [ + { + "command": "./scripts/nudge-1password-import", + "matcher": "append_variables|create_environment|create_local_env_file|list_variables" + } + ], + "postToolUse": [ + { + "command": "./scripts/nudge-1password-import", + "matcher": "MCP:append_variables|MCP:create_environment|MCP:create_local_env_file|MCP:list_variables" + } + ], + "stop": [ + { + "command": "./scripts/nudge-1password-import", + "loop_limit": 3 + } ] } } diff --git a/rules/1password-environments.mdc b/rules/1password-environments.mdc new file mode 100644 index 0000000..731d8b7 --- /dev/null +++ b/rules/1password-environments.mdc @@ -0,0 +1,34 @@ +--- +description: >- + 1Password Developer Environments — import, mount, or manage repo .env secrets + via the 1password MCP server. Apply before any 1Password MCP call or when the + user mentions 1Password environments, .env import/migrate/mount, or project secrets. +alwaysApply: false +--- + +# 1Password Environments (Cursor plugin) + +Read `skills/1password-environments/SKILL.md` once at the start of the turn, then execute. Do not read MCP tool descriptor files under `mcps/`. Do not fetch `1password://docs/getting-started` or `environments-guide` — they omit import-and-mount steps. + +## Done means + +| User intent | Done when | +|-------------|-----------| +| Import / create from `.env` | Variables in 1Password **and** `create_local_env_file` mount verified via `list_local_env_files` | +| Mount existing Environment | Mount exists at requested path (`list_local_env_files`) | +| List / rename / add vars | Requested MCP action completed | + +**Import is not done after `append_variables`.** `list_variables` does not verify a mount. + +## Import sequence (no shortcuts) + +1. `authenticate` → store `accountId` +2. Parse source `.env` with shell `grep` (never Read on `.env`) +3. `create_environment` or resolve existing → `append_variables` +4. `list_local_env_files` — skip `create_local_env_file` if mount already at path +5. `create_local_env_file` at source absolute path (workspace `/.env` when user says "here" / "project root") +6. Verify with `list_local_env_files` again + +Do not tell the user the task is complete, and do not offer mounting as an optional follow-up, until step 6 passes. + +Mount conflict (Read hook vs shell validation): see `skills/1password-environments/reference.md`. diff --git a/scripts/deny-env-file-read b/scripts/deny-env-file-read index 27b1a37..5af3229 100755 --- a/scripts/deny-env-file-read +++ b/scripts/deny-env-file-read @@ -26,13 +26,11 @@ _ENV_READ_ALLOWLIST = frozenset( _READ_TOOL_NAME = re.compile(r"^Read(?: File V2)?$", re.IGNORECASE) DENY_MESSAGE = ( - "Do not use the Read tool on .env files. Cursor often blocks or hangs on " - ".env reads — including plain text files on disk, not only 1Password mounts. " - "Mounted .env files are also named pipes that block until secrets stream. " - "To import variables into 1Password, parse with a shell command such as " - 'grep -E \'^[A-Za-z_][A-Za-z0-9_]*=\' "$path" | grep -v \'^#\', ask the ' - "user to paste KEY=value lines, or use list_local_env_files and MCP tools " - "for existing 1Password mounts." + "Do not use the Read tool on .env files. Parse with shell grep instead, " + "then finish the 1password-environments import workflow: append_variables, " + "create_local_env_file at the source path, verify with list_local_env_files. " + 'Example: grep -E \'^[A-Za-z_][A-Za-z0-9_]*=\' "$path" | grep -v \'^#\'. ' + "For mount conflicts see the plugin skill reference.md." ) diff --git a/scripts/nudge-1password-import b/scripts/nudge-1password-import new file mode 100755 index 0000000..10fd0aa --- /dev/null +++ b/scripts/nudge-1password-import @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Track 1Password import progress and nudge the agent to finish mounting. + +After append_variables during an import, many agents stop early. This hook +injects additional_context (after MCP) and, if the agent still tries to finish, +a stop-hook followup_message until create_local_env_file clears pending state. +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + +STATE_DIR = Path("/tmp/1password-cursor-plugin/import-state") + +APPEND_NUDGE = ( + "1Password import checkpoint: append_variables succeeded but the import is NOT " + "complete. Required next MCP calls in order: (1) list_local_env_files — skip " + "create_local_env_file if the source path is already mounted; (2) " + "create_local_env_file with mountPath set to the source .env absolute path " + "(project root /.env when the user said here or project root); (3) " + "list_local_env_files again to verify. list_variables does NOT verify a mount. " + "Do NOT tell the user the task is done. Do NOT offer mounting as an optional " + "follow-up." +) + +LIST_VARS_NUDGE = ( + "1Password import still incomplete: listing variable names does not create or " + "verify a local mount. If you imported from a .env file, call " + "list_local_env_files then create_local_env_file at the source path, then verify " + "with list_local_env_files before responding to the user." +) + +STOP_FOLLOWUP = ( + "Continue the 1Password .env import from where you left off: call " + "list_local_env_files, then create_local_env_file at the source .env absolute " + "path for this workspace, then verify with list_local_env_files. Read the " + "1password-environments plugin skill if needed. Do not reply to the user until " + "the mount is verified or the user explicitly declined mounting." +) + +_CREATE_ENV = re.compile(r"create_environment", re.IGNORECASE) +_APPEND_VARS = re.compile(r"append_variables", re.IGNORECASE) +_LIST_VARS = re.compile(r"list_variables", re.IGNORECASE) +_CREATE_MOUNT = re.compile(r"create_local_env_file", re.IGNORECASE) + + +def _debug(message: str) -> None: + if os.environ.get("DEBUG"): + STATE_DIR.mkdir(parents=True, exist_ok=True) + with (STATE_DIR / "nudge.log").open("a", encoding="utf-8") as log: + log.write(message + "\n") + + +def _tool_name(data: dict) -> str: + for key in ("tool_name", "toolName"): + value = data.get(key) + if isinstance(value, str) and value: + return value + return "" + + +def _normalize_tool_name(name: str) -> str: + base = name.rsplit("-", 1)[-1] + return base.lower() + + +def _parse_tool_input(data: dict) -> dict: + raw = data.get("tool_input") + if isinstance(raw, dict): + return raw + if isinstance(raw, str) and raw.strip(): + try: + parsed = json.loads(raw) + if isinstance(parsed, dict): + return parsed + except json.JSONDecodeError: + pass + return {} + + +def _mcp_succeeded(data: dict) -> bool: + raw = data.get("result_json") + if not isinstance(raw, str) or not raw.strip(): + return True + try: + result = json.loads(raw) + except json.JSONDecodeError: + return True + if isinstance(result, dict): + if result.get("error"): + return False + if result.get("isError") is True: + return False + return True + + +def _state_path(conversation_id: str) -> Path: + safe = re.sub(r"[^A-Za-z0-9._-]", "_", conversation_id or "unknown") + return STATE_DIR / f"{safe}.json" + + +def _load_state(conversation_id: str) -> dict: + path = _state_path(conversation_id) + if not path.is_file(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except (OSError, json.JSONDecodeError): + return {} + + +def _save_state(conversation_id: str, state: dict) -> None: + if not conversation_id: + return + STATE_DIR.mkdir(parents=True, exist_ok=True) + _state_path(conversation_id).write_text(json.dumps(state, indent=2), encoding="utf-8") + + +def _clear_state(conversation_id: str) -> None: + path = _state_path(conversation_id) + try: + path.unlink(missing_ok=True) + except OSError: + pass + + +def _variable_count(tool_input: dict) -> int: + variables = tool_input.get("variables") + return len(variables) if isinstance(variables, list) else 0 + + +def _should_track_mount(state: dict, tool_input: dict) -> bool: + if state.get("created_environment_this_session"): + return True + return _variable_count(tool_input) >= 2 + + +def _emit(additional_context: str | None = None, followup_message: str | None = None) -> None: + payload: dict[str, str] = {} + if additional_context: + payload["additional_context"] = additional_context + if followup_message: + payload["followup_message"] = followup_message + if payload: + print(json.dumps(payload)) + else: + print("{}") + + +def _handle_after_mcp(data: dict) -> None: + conversation_id = data.get("conversation_id") or data.get("session_id") or "" + tool = _normalize_tool_name(_tool_name(data)) + tool_input = _parse_tool_input(data) + + if not _mcp_succeeded(data): + _emit() + return + + state = _load_state(conversation_id) + + if _CREATE_ENV.search(tool): + state["created_environment_this_session"] = True + _save_state(conversation_id, state) + _emit() + return + + if _CREATE_MOUNT.search(tool): + _clear_state(conversation_id) + _emit() + return + + if _APPEND_VARS.search(tool): + if _should_track_mount(state, tool_input): + state["pending_mount"] = True + workspace_roots = data.get("workspace_roots") + if isinstance(workspace_roots, list) and workspace_roots: + state["workspace_root"] = workspace_roots[0] + for key in ("accountId", "environmentId", "environmentName"): + if key in tool_input: + state[key] = tool_input[key] + _save_state(conversation_id, state) + _emit(additional_context=APPEND_NUDGE) + return + _save_state(conversation_id, state) + _emit() + return + + if _LIST_VARS.search(tool) and state.get("pending_mount"): + _emit(additional_context=LIST_VARS_NUDGE) + return + + _emit() + + +def _handle_stop(data: dict) -> None: + conversation_id = data.get("conversation_id") or data.get("session_id") or "" + state = _load_state(conversation_id) + if state.get("pending_mount"): + _emit(followup_message=STOP_FOLLOWUP) + return + _emit() + + +def main() -> None: + raw = sys.stdin.read() + if not raw.strip(): + _emit() + return + + try: + data = json.loads(raw) + except json.JSONDecodeError: + _emit() + return + + event = data.get("hook_event_name") or "" + _debug(f"{event} tool={_tool_name(data)!r}") + + if event == "stop": + _handle_stop(data) + return + + if event in ("afterMCPExecution", "postToolUse"): + _handle_after_mcp(data) + return + + _emit() + + +if __name__ == "__main__": + main() diff --git a/skills/1password-environments/SKILL.md b/skills/1password-environments/SKILL.md index aff0f25..23bc8fe 100644 --- a/skills/1password-environments/SKILL.md +++ b/skills/1password-environments/SKILL.md @@ -1,134 +1,87 @@ --- name: 1password-environments description: >- - Provides the canonical Cursor agent workflow for 1Password Developer Environments - via MCP — supersedes the MCP server's getting-started and environments-guide - resources, which omit import and mount steps. Use when the user asks to create, - import, migrate, or mount plain .env files with 1Password, configure repo secrets, - list Environment variable names, manage Developer Environments, or call any - 1Password MCP tool. Requires reading this skill in full before the first MCP call - in a turn. + Canonical workflow for 1Password Developer Environments in Cursor via MCP. + Use when creating, importing, migrating, or mounting .env files; managing repo + secrets; listing Environment variable names; or calling any 1password MCP tool. + Read this entire file once before the first MCP call in the turn. --- # 1Password Environments -**MUST:** Read this entire skill before calling any 1Password MCP tool. +## Not done until -This skill ships with the **1Password Cursor plugin** (hooks + MCP config + this skill). It is the **authoritative agent workflow** for Developer Environments in Cursor. +**Import / create from `.env`** (including "using values from the project `.env`"): -## Quick start - -1. Read this skill (required before MCP calls). -2. **Route by intent:** - - On-disk `.env` to import, migrate, or populate from → [Import from a plain-text `.env` file](#import-from-a-plain-text-env-file) (through mount). - - Mount an existing Environment → [Mount 1Password as this repo's `.env`](#mount-1password-as-this-repos-env). - - New empty Environment (name only) → [Create a new Environment](#create-a-new-environment). - - Add or update specific variables → [Add or update variables](#add-or-update-variables). - - Inspect names only → [Inspect variables](#inspect-variables). -3. Call `authenticate`, then proceed with the chosen flow. - -## Gotchas - -- **MCP built-in docs are incomplete.** The server may point to `1password://docs/getting-started` and `1password://docs/environments-guide`. Those cover tool basics only — not import or default mount. Follow **this skill**, not those resources alone. -- **Install the plugin, not MCP alone.** Manual MCP setup without the plugin leaves agents without this workflow. -- **"Create from `.env`" is an import.** Do not stop after `create_environment` and `append_variables`; finish with `create_local_env_file` unless the user declines. -- **Do not Read secret `.env` paths.** Parse with shell `grep`. Templates (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`) may be read normally. - -## MCP tools - -| Tool | Description | -|------|-------------| -| `authenticate` | Authenticate with the 1Password desktop app; returns `accountId` | -| `list_environments` | List Developer Environments for an account | -| `create_environment` | Create a new Developer Environment | -| `rename_environment` | Rename an existing Developer Environment | -| `list_variables` | List variable names in an Environment (no values) | -| `append_variables` | Add or update Environment variables | -| `create_local_env_file` | Mount an Environment as a local `.env` file (macOS/Linux) | -| `list_local_env_files` | List existing local `.env` mounts for an Environment | - -MCP tool parameters use camelCase (`accountId`, `environmentId`). Server responses may use snake_case (`account_id`). - -## Common flows - -### Authenticate and resolve an Environment - -Run at the start of any turn unless you already hold both IDs. - -1. Call `authenticate` (desktop app prompts for approval on first connection). -2. Store `accountId`. Call `list_environments` unless the user provided a current `environmentId`. -3. Pick the target Environment from the user's request; ask if ambiguous — do not guess. -4. Store `environmentId`. +- [ ] Environment created or resolved +- [ ] Variables appended via `append_variables` +- [ ] `create_local_env_file` at the **source** `.env` absolute path +- [ ] Mount verified with `list_local_env_files` -### Mount 1Password as this repo's `.env` +Stopping after `create_environment` + `append_variables` is wrong. `list_variables` is not mount verification. -1. Authenticate and resolve the Environment. -2. Path: workspace root + `/.env` when the user says "here", "this repo", or "this project"; otherwise use their path. -3. Follow step 6 of [Import from a plain-text `.env` file](#import-from-a-plain-text-env-file). +**Mount only:** mount exists at the requested path (`list_local_env_files`). -### Inspect variables +## Do not -1. Authenticate and resolve the Environment. -2. Call `list_variables`. Summarize names only — never request or display values. +- Read files under `mcps/**/tools/*.json` — call MCP tools directly +- Fetch `1password://docs/getting-started` or `environments-guide` — they omit import-and-mount; this skill is the workflow +- Read secret `.env` paths — parse with `grep`; templates (`.env.example`, etc.) may be Read +- Report success or offer "want me to mount?" before the import checklist is complete -### Create a new Environment +## MCP tools -For a **new empty Environment** only (name, no `.env` import). +Parameters use camelCase (`accountId`, `environmentId`). Responses may use snake_case. -1. Call `authenticate` and store `accountId`. -2. Confirm the name if not explicitly stated. -3. Call `create_environment` with `accountId` and the chosen name. -4. Store `environmentId`. If variables come from a file, switch to the import flow. +| Tool | Use | +|------|-----| +| `authenticate` | First call each turn; returns `accountId` | +| `list_environments` | Resolve Environment by name | +| `create_environment` | New empty Environment (import continues below) | +| `rename_environment` | Rename | +| `list_variables` | Names only — never values | +| `append_variables` | `{ name, value, concealed }` — secrets `concealed: true` | +| `list_local_env_files` | Check existing mounts | +| `create_local_env_file` | Mount at `mountPath` (macOS/Linux) | -### Import from a plain-text `.env` file +## Import from a plain `.env` file -For creating or populating an Environment from a **regular file** on disk (`.env`, `.env.local`, or "values from the project `.env`"). No MCP import tool — parse locally, then `append_variables`. **Mount at the source path by default** (step 6). +Default path: `{workspace_root}/.env` unless the user names another path. -1. Confirm source path; migration includes create, append, and mount unless the user opts out. -2. Verify plain file: `test -f "$path" && ! test -p "$path"`. If FIFO, stop — use `list_local_env_files` instead. -3. Parse (do not Read secret paths): +1. **Plain file check:** `test -f "$path" && ! test -p "$path"`. If FIFO, use `list_local_env_files` instead of importing from disk. +2. **Parse** (do not Read the secret path): ```bash grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$path" | grep -v '^#' ``` - Strip optional quotes before MCP; do not paste raw output into chat. -4. `authenticate`, then `create_environment` or resolve an existing Environment. -5. `append_variables` with `{ "name", "value", "concealed" }` — `true` for secrets, `false` for non-sensitive config. Pass values to MCP only. -6. **Mount** at the source absolute path (required unless declined): - - `list_local_env_files` — skip if mount already exists at path. - - `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, `mountPath`. - - Verify with `list_local_env_files`, not by reading the mounted file. - -**Import completion checklist:** + Strip optional quotes; pass values to MCP only — do not paste into chat. +3. **`authenticate`** → `accountId` +4. **`create_environment`** (new name) or **`list_environments`** (existing) +5. **`append_variables`** with all parsed variables +6. **Mount** (required unless user declined): + - `list_local_env_files` — skip if mount already at path + - `create_local_env_file` with `accountId`, `environmentId`, `environmentName`, `mountPath` (absolute source path) + - `list_local_env_files` again to verify -- [ ] Environment created or resolved -- [ ] Variables appended -- [ ] Mount created at source path (unless user declined) -- [ ] Mount verified with `list_local_env_files` +If shell `grep` is blocked, see [reference.md](reference.md) (mount conflict). -If shell parsing is blocked, see [reference.md](reference.md) (mount conflict and troubleshooting). +## Other flows -### Rename an Environment +**Mount existing Environment:** authenticate → resolve Environment → step 6 above. -1. Authenticate and resolve the Environment. -2. Confirm the new name if not stated. -3. Call `rename_environment` with `accountId`, `environmentId`, and the new name. +**Inspect names:** authenticate → resolve → `list_variables` → summarize names only. -### Add or update variables +**Rename:** authenticate → resolve → confirm name → `rename_environment`. -1. Confirm the user wants to create or update variables; collect missing names/values. -2. Authenticate and resolve the Environment. -3. `list_variables` to check existing names. -4. `append_variables` with `{ "name", "value", "concealed" }`. +**Add/update variables:** authenticate → resolve → `list_variables` → `append_variables`. ## Safety -- Do not reveal, log, or echo secret values in chat. -- Do not Read secret `.env` paths or mounted FIFO `.env` files; use MCP tools for mounted files. -- Ask before creating or modifying variables unless the request is explicit ("import my `.env`", "add API_KEY to staging"). -- If the user pasted a secret, refer to it by variable name only. +- Never reveal secret values in chat +- Never Read mounted FIFO `.env` files — use MCP for mounts +- Ask before changing variables unless the request is explicit -## Additional resources +## Troubleshooting -- Mount conflict, validation modes, and setup troubleshooting: [reference.md](reference.md) +Mount conflict, validation modes, setup: [reference.md](reference.md) diff --git a/skills/1password-environments/reference.md b/skills/1password-environments/reference.md index 87a2f76..b9aacf3 100644 --- a/skills/1password-environments/reference.md +++ b/skills/1password-environments/reference.md @@ -1,5 +1,9 @@ # 1Password Environments — reference +## Import completion nudges (plugin hook) + +The plugin runs `scripts/nudge-1password-import` after relevant 1Password MCP calls and on agent stop. If you appended variables during an import but have not called `create_local_env_file`, the hook injects the next required steps. Treat that as mandatory — do not reply to the user until the mount is verified. + ## Mount conflict (Read hook vs shell validation hook) The Read hook denies `.env` reads; the `beforeShellExecution` hook blocks **all** shell commands when 1Password expects a mount at a path that is missing, disabled, or not a FIFO (for example a plain `.env` still on disk at the mount path). That is a policy deadlock: Read is denied, then shell is denied too. From 69aca5cfa5b5bbd3778433b99fd947eda0cabb6f Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 16:09:06 -0700 Subject: [PATCH 09/10] updating url --- .cursor-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 72d8475..8eadb70 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -5,7 +5,7 @@ "author": { "name": "1Password" }, - "homepage": "https://developer.1password.com/docs/environments", + "homepage": "https://www.1password.dev/environments", "repository": "https://github.com/1Password/cursor-plugin", "license": "MIT", "keywords": [ From 3353071ada78c28cf6176c19d323d210a3662d1f Mon Sep 17 00:00:00 2001 From: Scott Lougheed Date: Mon, 29 Jun 2026 16:09:56 -0700 Subject: [PATCH 10/10] updating url --- .cursor-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 8eadb70..4e95bd0 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -5,7 +5,7 @@ "author": { "name": "1Password" }, - "homepage": "https://www.1password.dev/environments", + "homepage": "https://www.1password.dev/", "repository": "https://github.com/1Password/cursor-plugin", "license": "MIT", "keywords": [