can1357/oh-my-pi · error · Error
Security plan repository ${plan.repositoryRoot} does not mat
Error message
Security plan repository ${plan.repositoryRoot} does not match ${this.#repositoryRoot} What it means
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.
Source
Thrown at packages/coding-agent/src/security/store.ts:281
}
// The scan manifest is the commit marker: readers never observe it before
// its findings and optional artifacts have been written atomically.
await writeSecurityFileAtomic(path.join(scanDirectory, "scan.json"), `${JSON.stringify(bundle.scan, null, 2)}\n`);
const index = await this.#readIndex();
if (!index.scanIds.includes(bundle.scan.id)) index.scanIds.push(bundle.scan.id);
index.updatedAt = new Date().toISOString();
await this.#writeIndex(index);
}
async putBundle(input: SecurityScanBundle): Promise<void> {
await withSecurityStoreWrite(this.#projectDirectory, () => this.#putBundleUnlocked(input));
}
async putPlan(input: SecurityScanPlan): Promise<void> {
await withSecurityStoreWrite(this.#projectDirectory, async () => {
const plan = parseSecurityScanPlan(input);
if (plan.repositoryRoot !== this.#repositoryRoot) {
throw new Error(`Security plan repository ${plan.repositoryRoot} does not match ${this.#repositoryRoot}`);
}
await writeSecurityFileAtomic(this.#planPath(plan.id), `${JSON.stringify(plan, null, 2)}\n`);
const index = await this.#readIndex();
if (!index.planIds.includes(plan.id)) index.planIds.push(plan.id);
index.updatedAt = new Date().toISOString();
await this.#writeIndex(index);
});
}
async getPlan(planId: string): Promise<SecurityScanPlan | null> {
try {
return parseSecurityScanPlan(await readJsonFile(this.#planPath(planId)));
} catch (error) {
if (isEnoent(error)) return null;
throw error;
}
}
View on GitHub (pinned to 9690622007)
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.
Example fix
// before plan.repositoryRoot = '/tmp/work'; // symlink await store.putPlan(plan); // after plan.repositoryRoot = store.repositoryRoot; // canonical realpath, e.g. /private/tmp/work await store.putPlan(plan);
Defensive patterns
Strategy: validation
Validate before calling
if (plan.repositoryRoot !== store.repositoryRoot) {
plan.repositoryRoot = store.repositoryRoot; // or route to the matching store
} Type guard
function planBelongsToStore(plan: SecurityScanPlan, store: SecurityStore): boolean {
return plan.repositoryRoot === store.repositoryRoot;
} Try / catch
try {
await store.putPlan(plan);
} catch (err) {
if (err instanceof Error && err.message.includes('does not match')) {
plan.repositoryRoot = store.repositoryRoot;
await store.putPlan(plan);
} else throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid security store plan index at ${this.#indexPath()}
- Security scan project key ${bundle.scan.projectKey} does not
- Invalid findings list for ${scanId}
- Unknown security scan: ${scanId}
- Unknown security finding: ${findingId}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/75e28f47e93ee9e1.
Report an issue: GitHub.