abhigyanpatwari/GitNexus · error · UnsafeStoragePathError
Unsafe storage path for
Error message
Unsafe storage path for ${entry.path}: expected ${error.expectedStoragePath}, actual ${error.actualStoragePath} What it means
UnsafeStoragePathError is thrown by assertSafeStoragePath when the underlying requireDeletableStoragePath check raises StorageDeletionError. It means the registry entry's on-disk storage slot does not match the expected slot path computed for that repository entry (expectedStoragePath vs actualStoragePath). GitNexus refuses to delete or mutate storage when the resolved path is not exactly the isolated slot it computed, to prevent deleting the wrong directory.
Solutions
- Print error.expectedStoragePath and error.actualStoragePath and reconcile the registry entry's storagePath to the expected value (re-run `gitnexus analyze` / re-register the repo).
- Verify the on-disk directory at actualStoragePath: if it is the real data, move it to expectedStoragePath rather than editing code.
- Re-check symlink/case canonicalization of the repository path with path.resolve(realpathSync(repoPath)) so canonicalRepoPath matches.
- As a last resort remove the stale slot and re-index from scratch with `node .gitnexus/run.cjs analyze --index-only`.
Example fix
// before
await assertSafeStoragePath({ ...entry, storagePath: '/external/repos/repo-a-abc123' });
// after
const canonical = await canonicalRepoPath(entry.repoPath);
await assertSafeStoragePath({ ...entry, storagePath: storagePathFromRoot(storageRoot, canonical) }); Defensive patterns
Strategy: try-catch
Validate before calling
const expected = storagePathFromRoot(storageRoot, await canonicalRepoPath(entry.repoPath));
if (entry.storagePath !== undefined && path.resolve(entry.storagePath) !== path.resolve(expected)) {
throw new Error(`registry storagePath ${entry.storagePath} != expected ${expected}; re-register repo`);
} Type guard
const isRegistryEntry = (e: unknown): e is RegistryEntry => typeof e === 'object' && e !== null && typeof (e as any).repoPath === 'string';
Try / catch
try {
await assertSafeStoragePath(entry);
} catch (e) {
if (e instanceof UnsafeStoragePathError) {
console.error(`storage slot mismatch for ${entry.repoPath}: expected ${e.expectedStoragePath}, found ${e.actualStoragePath} — re-register before deleting`);
}
throw e;
} Prevention
- Never hand-edit .gitnexus registry rows; use the CLI to re-register.
- After moving a .gitnexus directory, always re-run analyze to refresh storagePath.
- Canonicalize repo paths (realpath) before registering so slot derivation is stable.
When it happens
Trigger: Calling assertSafeStoragePath(entry) (or a deletion flow that uses it) where the RegistryEntry's stored/derived storagePath differs from the canonical expected slot path, e.g. the registry's storagePath was moved, renamed, or populated by an older version of the resolver.
Common situations: Manually editing .gitnexus registry rows or moving the .gitnexus directory; a repo re-registered from a different canonical path casing/symlink; an upgrade changed slot naming so legacy rows carry stale paths.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- err.message
- Analysis not finalized: missing
- Could not remove the shadowed branch sub-index; keeping its…
- DiskBackedScopeTree.byId is unsupported…
- entry does not identify a repo — name and storagePath must…
AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-09-15).
Data as JSON: /api/errors/386ed3024f45486f.
Report an issue: GitHub.
Appendix: source
Thrown at gitnexus/src/storage/repo-manager.ts:1553
* re-derives storagePath from `findRepo(cwd)` and never trusts
* the registry field. But `clean --all` DOES iterate the registry
* and trust each entry's stored storagePath (same shape as
* `remove`), so this helper must be wired into that loop too.
* - An external slot is intentionally not constrained under a checkout. Its
* own metadata must bind both the source checkout path and the resolved
* storage path before it may be removed.
*
* The resolver preserves the legacy local-path allowance while also rejecting
* foreign or malformed metadata. External slots additionally require metadata
* ownership so a hand-edited registry cannot redirect a destructive command to
* an arbitrary directory.
*/
export const assertSafeStoragePath = async (entry: RegistryEntry): Promise<void> => {
try {
await requireDeletableStoragePath(entry);
} catch (error) {
if (error instanceof StorageDeletionError) {
throw new UnsafeStoragePathError(entry, error.expectedStoragePath, error.actualStoragePath);
}
throw error;
}
};
/**
* Resolve a user-supplied target string (from `gitnexus remove <target>`
* or equivalent MCP tool argument) to a single registry entry.
*
* Match precedence (first hit wins, subsequent tiers are only tried if
* the prior tier produces zero matches):
* 1. Exact resolved-path match (Windows: case-insensitive).
* Paths are unique by registry construction, so a path match can
* never be ambiguous.
* 2. Exact `name` match (case-insensitive). If ≥ 2 entries share the
* name — only possible via `--allow-duplicate-name` (#829) —
* throws {@link RegistryAmbiguousTargetError}.
*View on GitHub (pinned to ac9a4e9abd)