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

  1. Set plan.repositoryRoot to the canonical resolved root: use store.repositoryRoot (the value the store was opened with) when constructing the plan.
  2. Open the SecurityStore with the plan's own repositoryRoot so the comparison uses that root.
  3. Resolve symlinks with fs.realpath before assigning repositoryRoot, mirroring what SecurityStore.open() does.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/75e28f47e93ee9e1. Report an issue: GitHub.