{"record":{"id":"75e28f47e93ee9e1","repo":"can1357/oh-my-pi","slug":"security-plan-repository-plan-repositoryroot-do","errorCode":null,"errorMessage":"Security plan repository ${plan.repositoryRoot} does not match ${this.#repositoryRoot}","messagePattern":"Security plan repository (.+?) does not match (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/security/store.ts","lineNumber":281,"sourceCode":"\t\t}\n\t\t// The scan manifest is the commit marker: readers never observe it before\n\t\t// its findings and optional artifacts have been written atomically.\n\t\tawait writeSecurityFileAtomic(path.join(scanDirectory, \"scan.json\"), `${JSON.stringify(bundle.scan, null, 2)}\\n`);\n\t\tconst index = await this.#readIndex();\n\t\tif (!index.scanIds.includes(bundle.scan.id)) index.scanIds.push(bundle.scan.id);\n\t\tindex.updatedAt = new Date().toISOString();\n\t\tawait this.#writeIndex(index);\n\t}\n\n\tasync putBundle(input: SecurityScanBundle): Promise<void> {\n\t\tawait withSecurityStoreWrite(this.#projectDirectory, () => this.#putBundleUnlocked(input));\n\t}\n\n\tasync putPlan(input: SecurityScanPlan): Promise<void> {\n\t\tawait withSecurityStoreWrite(this.#projectDirectory, async () => {\n\t\t\tconst plan = parseSecurityScanPlan(input);\n\t\t\tif (plan.repositoryRoot !== this.#repositoryRoot) {\n\t\t\t\tthrow new Error(`Security plan repository ${plan.repositoryRoot} does not match ${this.#repositoryRoot}`);\n\t\t\t}\n\t\t\tawait writeSecurityFileAtomic(this.#planPath(plan.id), `${JSON.stringify(plan, null, 2)}\\n`);\n\t\t\tconst index = await this.#readIndex();\n\t\t\tif (!index.planIds.includes(plan.id)) index.planIds.push(plan.id);\n\t\t\tindex.updatedAt = new Date().toISOString();\n\t\t\tawait this.#writeIndex(index);\n\t\t});\n\t}\n\n\tasync getPlan(planId: string): Promise<SecurityScanPlan | null> {\n\t\ttry {\n\t\t\treturn parseSecurityScanPlan(await readJsonFile(this.#planPath(planId)));\n\t\t} catch (error) {\n\t\t\tif (isEnoent(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/security/store.ts#L263-L299","documentation":"putPlan() validates that a SecurityScanPlan's repositoryRoot equals the canonical repository root this store was constructed with. The store scopes all plans to one repository; storing a plan for a different root would let remediation plans leak across projects, so the write is aborted before touching disk.","triggerScenarios":"Calling putPlan(plan) where plan.repositoryRoot differs from store.repositoryRoot — e.g. the plan was generated against a symlinked or differently-cased path, a moved/cloned repo, or a non-canonical path while the store stores the realpath-resolved root.","commonSituations":"Generating plans on macOS with /tmp vs /private/tmp (symlink) or case-insensitive path casing; repo accessed via symlink; plan shared from a teammate's checkout path; opening the store with SecurityStore.open() (which realpaths) while the plan holds the raw path.","solutions":["Set plan.repositoryRoot to the canonical resolved root: use store.repositoryRoot (the value the store was opened with) when constructing the plan.","Open the SecurityStore with the plan's own repositoryRoot so the comparison uses that root.","Resolve symlinks with fs.realpath before assigning repositoryRoot, mirroring what SecurityStore.open() does.","Only if the plan genuinely belongs elsewhere, persist it via that repo's own store instance instead."],"exampleFix":"// before\nplan.repositoryRoot = '/tmp/work'; // symlink\nawait store.putPlan(plan);\n// after\nplan.repositoryRoot = store.repositoryRoot; // canonical realpath, e.g. /private/tmp/work\nawait store.putPlan(plan);","handlingStrategy":"validation","validationCode":"if (plan.repositoryRoot !== store.repositoryRoot) {\n  plan.repositoryRoot = store.repositoryRoot; // or route to the matching store\n}","typeGuard":"function planBelongsToStore(plan: SecurityScanPlan, store: SecurityStore): boolean {\n  return plan.repositoryRoot === store.repositoryRoot;\n}","tryCatchPattern":"try {\n  await store.putPlan(plan);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('does not match')) {\n    plan.repositoryRoot = store.repositoryRoot;\n    await store.putPlan(plan);\n  } else throw err;\n}","preventionTips":["Assign plan.repositoryRoot from store.repositoryRoot, never from raw user paths.","fs.realpath() repository paths before building plans, mirroring SecurityStore.open().","Beware symlinks (/tmp vs /private/tmp) and path casing when generating plans.","Route plans for other repositories to their own SecurityStore instances."],"tags":["security-store","path-mismatch","repository-root"],"backgroundTag":"project-key-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}