Bounty eligibility
Stage
configure / manage secrets
Coding agent used
Claude Code (Monk plugin v0.1.52)
Summary
secret.add and secret.list accept a workspaceRoot argument, and every stored secret persists a per-secret workspaceRoot field — both of which imply that scope: "workspace" secrets are isolated per project directory. They are not.
Every scope: "workspace" secret created within an agent session is written under a single, session-bound workspaceId, regardless of the workspaceRoot passed. Consequently, workspace-scoped secrets belonging to different project roots are pooled together and cross-list: listing secrets while pointing at workspace A returns secrets that were added for workspace B, and vice-versa. The workspaceRoot argument is silently accepted but never used to resolve/scope the operation, and the persisted workspaceRoot metadata ends up inconsistent with the workspaceId the secret is actually stored under.
Repro steps (via the plugin tools only)
Let A and B be two unrelated project directories (in my run: A = C:\Users\<me>\Desktop\Data, the session's default root, and B = C:\Users\<me>\.hermes\monk-test-app).
- Add a workspace secret without an explicit root (uses session default root
A):
secret.add { name: "hpx", value: "m2", scope: "workspace" }
- Add another workspace secret with an explicit, different root
B:
secret.add { name: "iso_probe_mta", value: "mta1", scope: "workspace", workspaceRoot: "<B>" }
- List with
workspaceRoot = B:
secret.list { scope: "workspace", workspaceRoot: "<B>" }
→ returns both hpx (added under A) and iso_probe_mta.
- List with
workspaceRoot = A:
secret.list { scope: "workspace", workspaceRoot: "<A>" }
→ also returns both.
Expected: list(B) returns only secrets added for B; list(A) only those for A.
Actual: both lists return the full pooled set of the user's workspace secrets.
Reproduced / cross-verified (three independent sources agree)
1) Tool output — secret.list with workspaceRoot=A and with workspaceRoot=B returned the identical full set. Four test secrets were added (hpx alpha beta, hpx, HPX under root A; iso_probe_mta under root B); both list calls returned all four:
{ "secrets": [
{ "name": "hpx alpha beta", "scope": "workspace", "source": "manual", ... },
{ "name": "hpx", "scope": "workspace", "source": "manual", ... },
{ "name": "HPX", "scope": "workspace", "source": "manual", ... },
{ "name": "iso_probe_mta", "scope": "workspace", "source": "manual", ... }
] }
2) Local vault index (~/.monk/agent/store/global/credentials/index%<base64 userId>.json) — four entries; three carry "workspaceRoot": "...\\Desktop\\Data" and one carries "workspaceRoot": "...\\.hermes\\monk-test-app", yet all four share the same "workspaceId": "ws_a6336bf25581d5f5e78d8155":
{ "name": "hpx", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "workspaceRoot": "C:\\Users\\<me>\\Desktop\\Data", ... }
{ "name": "iso_probe_mta", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "workspaceRoot": "C:\\Users\\<me>\\.hermes\\monk-test-app", ... }
3) Audit log (~/.monk/agent/store/audit/secret-access.json) — all four "Store secret via secret.add" write records, including the one added with workspaceRoot=...\monk-test-app, are logged under the same workspaceId:
{ "operation": "write", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "keyHash": "154e3490", ... }
{ "operation": "write", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "keyHash": "92dd85ed", ... } // iso_probe_mta, added with workspaceRoot=...\monk-test-app
Root cause (as far as visible from persisted state)
The workspaceId used to key workspace-scoped secrets is bound once to the agent session and reused for every secret operation. The workspaceRoot argument is copied into the per-secret metadata but is never used to resolve/compute the workspace, so it can neither scope reads nor isolate writes. The platform workspace registry (~/.monk/agent/store/global/platform-cache/workspaces_*.json) lists monk-test-app as its own distinct project, so the two roots are genuinely separate workspaces being collapsed onto one id.
Impact
- Workspace-scope isolation — advertised through the
workspaceRoot parameter and the per-secret workspaceRoot field — does not hold. A secret intended for project B is visible to a session operating in project A (same user). This defeats the purpose of a per-workspace secret scope.
- The persisted
workspaceRoot field is self-contradictory with the workspaceId it is stored under, so any tooling (or operator) that trusts that field is misled about which project a secret belongs to.
- Open question I did not verify (flagging for maintainers): the cluster scopes (
org/account/project/env) write to the shared Monk KV store. If they key by workspace with the same workspaceRoot-insensitive logic, the impact would extend beyond a single user's local vault to cross-project secret exposure on the platform. I tested only local workspace scope; this is worth checking.
Expected behavior
workspace scope should isolate by the resolved workspace: either honor workspaceRoot (resolve it to the correct workspaceId and scope reads/writes to it), or reject / warn when the provided workspaceRoot does not match the bound session workspace. It should not silently accept the argument, persist contradictory workspaceRoot metadata, and pool every workspace secret under one session id.
Actual behavior
workspaceRoot is accepted but ignored; all scope: "workspace" secrets in a session share one workspaceId and cross-list across different project roots.
Environment
- OS: Windows 11
- Monk CLI/daemon: v3.21.1, build 0ecb47b4
- Monk plugin (Claude Code): v0.1.52
Duplicate check
Searched open/closed issues for "secret workspace", "secret isolation", "workspace secrets", "secret scope", and "workspaceRoot". Found a diagnostics-path issue (#134, MANIFEST LOAD/DIRS paths outside workspaceRoot) and several "reports success when it fails" issues (#201/#202/#203/#204/#243), but none covering secret workspace-scope isolation or workspaceRoot not being honored by the secret tools.
Payout address
(N/A — cash prize is per contest ranking per the published rules, not per-bug on-chain payout; leaving this blank.)
Bounty eligibility
Stage
configure / manage secrets
Coding agent used
Claude Code (Monk plugin v0.1.52)
Summary
secret.addandsecret.listaccept aworkspaceRootargument, and every stored secret persists a per-secretworkspaceRootfield — both of which imply thatscope: "workspace"secrets are isolated per project directory. They are not.Every
scope: "workspace"secret created within an agent session is written under a single, session-boundworkspaceId, regardless of theworkspaceRootpassed. Consequently, workspace-scoped secrets belonging to different project roots are pooled together and cross-list: listing secrets while pointing at workspace A returns secrets that were added for workspace B, and vice-versa. TheworkspaceRootargument is silently accepted but never used to resolve/scope the operation, and the persistedworkspaceRootmetadata ends up inconsistent with theworkspaceIdthe secret is actually stored under.Repro steps (via the plugin tools only)
Let
AandBbe two unrelated project directories (in my run:A = C:\Users\<me>\Desktop\Data, the session's default root, andB = C:\Users\<me>\.hermes\monk-test-app).A):secret.add { name: "hpx", value: "m2", scope: "workspace" }B:secret.add { name: "iso_probe_mta", value: "mta1", scope: "workspace", workspaceRoot: "<B>" }workspaceRoot = B:secret.list { scope: "workspace", workspaceRoot: "<B>" }→ returns both
hpx(added underA) andiso_probe_mta.workspaceRoot = A:secret.list { scope: "workspace", workspaceRoot: "<A>" }→ also returns both.
Expected:
list(B)returns only secrets added forB;list(A)only those forA.Actual: both lists return the full pooled set of the user's workspace secrets.
Reproduced / cross-verified (three independent sources agree)
1) Tool output —
secret.listwithworkspaceRoot=Aand withworkspaceRoot=Breturned the identical full set. Four test secrets were added (hpx alpha beta,hpx,HPXunder rootA;iso_probe_mtaunder rootB); both list calls returned all four:{ "secrets": [ { "name": "hpx alpha beta", "scope": "workspace", "source": "manual", ... }, { "name": "hpx", "scope": "workspace", "source": "manual", ... }, { "name": "HPX", "scope": "workspace", "source": "manual", ... }, { "name": "iso_probe_mta", "scope": "workspace", "source": "manual", ... } ] }2) Local vault index (
~/.monk/agent/store/global/credentials/index%<base64 userId>.json) — four entries; three carry"workspaceRoot": "...\\Desktop\\Data"and one carries"workspaceRoot": "...\\.hermes\\monk-test-app", yet all four share the same"workspaceId": "ws_a6336bf25581d5f5e78d8155":{ "name": "hpx", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "workspaceRoot": "C:\\Users\\<me>\\Desktop\\Data", ... } { "name": "iso_probe_mta", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "workspaceRoot": "C:\\Users\\<me>\\.hermes\\monk-test-app", ... }3) Audit log (
~/.monk/agent/store/audit/secret-access.json) — all four"Store secret via secret.add"write records, including the one added withworkspaceRoot=...\monk-test-app, are logged under the sameworkspaceId:{ "operation": "write", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "keyHash": "154e3490", ... } { "operation": "write", "scope": "workspace", "workspaceId": "ws_a6336bf25581d5f5e78d8155", "keyHash": "92dd85ed", ... } // iso_probe_mta, added with workspaceRoot=...\monk-test-appRoot cause (as far as visible from persisted state)
The
workspaceIdused to key workspace-scoped secrets is bound once to the agent session and reused for every secret operation. TheworkspaceRootargument is copied into the per-secret metadata but is never used to resolve/compute the workspace, so it can neither scope reads nor isolate writes. The platform workspace registry (~/.monk/agent/store/global/platform-cache/workspaces_*.json) listsmonk-test-appas its own distinct project, so the two roots are genuinely separate workspaces being collapsed onto one id.Impact
workspaceRootparameter and the per-secretworkspaceRootfield — does not hold. A secret intended for projectBis visible to a session operating in projectA(same user). This defeats the purpose of a per-workspace secret scope.workspaceRootfield is self-contradictory with theworkspaceIdit is stored under, so any tooling (or operator) that trusts that field is misled about which project a secret belongs to.org/account/project/env) write to the shared Monk KV store. If they key by workspace with the sameworkspaceRoot-insensitive logic, the impact would extend beyond a single user's local vault to cross-project secret exposure on the platform. I tested only localworkspacescope; this is worth checking.Expected behavior
workspacescope should isolate by the resolved workspace: either honorworkspaceRoot(resolve it to the correctworkspaceIdand scope reads/writes to it), or reject / warn when the providedworkspaceRootdoes not match the bound session workspace. It should not silently accept the argument, persist contradictoryworkspaceRootmetadata, and pool every workspace secret under one session id.Actual behavior
workspaceRootis accepted but ignored; allscope: "workspace"secrets in a session share oneworkspaceIdand cross-list across different project roots.Environment
Duplicate check
Searched open/closed issues for "secret workspace", "secret isolation", "workspace secrets", "secret scope", and "workspaceRoot". Found a diagnostics-path issue (#134, MANIFEST LOAD/DIRS paths outside workspaceRoot) and several "reports success when it fails" issues (#201/#202/#203/#204/#243), but none covering secret workspace-scope isolation or
workspaceRootnot being honored by the secret tools.Payout address
(N/A — cash prize is per contest ranking per the published rules, not per-bug on-chain payout; leaving this blank.)