garrytan/gstack · error
invalid_allowlist
invalid_allowlist
Error message
invalid_allowlist
What it means
Thrown by loadAllowlist() when the iOS-QA allowlist JSON file at ~/.gstack/ios-qa-allowlist.json (or GSTACK_IOS_ALLOWLIST_PATH) parses successfully but is structurally invalid: parsed.version is not exactly 1, or parsed.entries is not an array. It is a deliberate fail-closed guard: a half-written or hand-edited file must not silently pass as an empty list. ENOENT and empty files are tolerated earlier; this fires only after a successful JSON.parse.
Source
Thrown at ios-qa/daemon/src/allowlist.ts:38
let raw: string;
try {
raw = await readFile(path, 'utf-8');
} catch (err: unknown) {
const e = err as { code?: string };
if (e.code === 'ENOENT') {
return { version: 1, entries: [] };
}
throw err;
}
// Empty-file path (mktemp default, partial write, manual `: > file`): treat
// as "no entries yet" rather than a parse error. The first grant will fill
// it in atomically via saveAllowlist.
if (raw.trim() === '') {
return { version: 1, entries: [] };
}
const parsed = JSON.parse(raw) as Allowlist;
if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
throw new Error('invalid_allowlist');
}
return parsed;
}
export async function saveAllowlist(allowlist: Allowlist, path: string = defaultAllowlistPath()): Promise<void> {
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
await writeFile(path, JSON.stringify(allowlist, null, 2) + '\n', { mode: 0o600 });
}
/**
* Look up an identity in the allowlist. Returns the entry if present AND
* not expired. Lookup is exact-match on canonicalized identity.
*/
export function findEntry(allowlist: Allowlist, identity: string): AllowlistEntry | null {
const now = Date.now();
for (const entry of allowlist.entries) {
if (entry.identity !== identity) continue;
if (entry.expires_at) {View on GitHub (pinned to 94993f7401)
Solutions
- Back up then delete ~/.gstack/ios-qa-allowlist.json and re-grant identities via gstack-ios-qa-mint grant.
- If you must edit by hand, ensure the shape is exactly {"version":1,"entries":[...]} with entries as an array.
- Do not bump version without updating loadAllowlist to handle the new schema.
- If the file was corrupted by a crash mid-write, restore from backup or regenerate.
Example fix
// before — corrupted file
{ "version": 2, "entries": {} }
// after — valid schema
{ "version": 1, "entries": [] } Defensive patterns
Strategy: validation
Validate before calling
import { readFile } from 'fs/promises';
async function preflightAllowlist(path: string): Promise<void> {
const raw = await readFile(path, 'utf-8');
const parsed = JSON.parse(raw); // throws on parse error
if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
throw new Error(`Allowlist at ${path} has invalid shape; expected {version:1, entries:[]}`);
}
} Type guard
function isAllowlist(v: unknown): v is { version: number; entries: unknown[] } {
return typeof v === 'object' && v !== null &&
(v as any).version === 1 && Array.isArray((v as any).entries);
} Try / catch
try {
return await loadAllowlist(path);
} catch (e) {
if (e instanceof Error && e.message === 'invalid_allowlist') {
// Back up the corrupt file, start fresh
await rename(path, path + '.bak');
return { version: 1, entries: [] };
}
throw e;
} Prevention
- Never hand-edit the allowlist JSON; use gstack-ios-qa-mint grant/revoke.
- If you must edit, validate shape with a JSON schema linter first.
- Back up the allowlist before any migration.
- Add a schema version check at app startup to fail fast on corruption.
When it happens
Trigger: JSON.parse succeeds but the top-level object lacks version:1 or has a non-array entries field. Concretely: someone hand-edited the file and dropped a key; a partial write left valid JSON but wrong shape; a future schema migration wrote version:2 without a loader; entries is an object/number instead of an array.
Common situations: Manual edit of the allowlist that broke the schema; a botched migration; an editor auto-saved a truncated object; wrong file placed at the allowlist path.
Related errors
- proxy creds set in both env (BROWSE_PROXY_USER) and URL — pi
- gbrain sources list returned no JSON
- Invalid skill name "${name}". Must be lowercase letters/digi
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: staged dir "${opts.stagedDir}" is a symlink — r
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/4c6cb1a9cfacf22f.
Report an issue: GitHub.