garrytan/gstack · error · Error
Chain rejected: subcommand "${c.rawName}" not allowed by you
Error message
Chain rejected: subcommand "${c.rawName}" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). All subcommands must be within scope. What it means
Before executing any subcommand, `chain` pre-validates EVERY entry against the caller's token scope using the canonical command name (lines 629-640). If the token is non-root and a subcommand is not in `tokenInfo.scopes`, the whole chain is rejected up front — no partial execution. This prevents privilege escalation by chaining an out-of-scope command behind an in-scope one.
Source
Thrown at browse/src/meta-commands.ts:634
}
// Canonicalize aliases across the whole chain. Pair canonical name with the raw
// input so result labels + error messages reflect what the user typed, but every
// dispatch path (scope check, WRITE_COMMANDS.has, watch blocking, handler lookup)
// uses the canonical name. Otherwise `chain '[["setcontent","/tmp/x.html"]]'`
// bypasses prevalidation or runs under the wrong command set.
const commands = rawCommands.map(cmd => {
const [rawName, ...cmdArgs] = cmd;
const name = canonicalizeCommand(rawName);
return { rawName, name, args: cmdArgs };
});
// Pre-validate ALL subcommands against the token's scope before executing any.
// Uses canonical name so aliases don't bypass scope checks.
if (tokenInfo && tokenInfo.clientId !== 'root') {
for (const c of commands) {
if (!checkScope(tokenInfo, c.name)) {
throw new Error(
`Chain rejected: subcommand "${c.rawName}" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). ` +
`All subcommands must be within scope.`
);
}
}
}
// Route each subcommand through handleCommandInternal for full security:
// scope, domain, tab ownership, content wrapping — all enforced per subcommand.
// Chain-specific options: skip rate check (chain = 1 request), skip activity
// events (chain emits 1 event), increment chain depth (recursion guard).
const executeCmd = opts?.executeCommand;
const results: string[] = [];
let lastWasWrite = false;
if (executeCmd) {
// Full security pipeline via handleCommandInternal.
// Pass rawName so the server's own canonicalization is a no-op (already canonical).View on GitHub (pinned to 94993f7401)
Solutions
- Inspect the token's scopes (the message lists them) and remove subcommands not present.
- Re-issue the token with the missing scope if the action is intended.
- Use a `root` token only if scope isolation is not required.
- Verify the canonical name — aliases are normalized before the scope check, so an alias for an out-of-scope canonical command still fails.
Example fix
// before: token scope = ["read"] browse chain '[["goto","https://x"],["click","@e5"]]' // click not in scope // after browse chain '[["goto","https://x"],["text"]]' // text is in scope
Defensive patterns
Strategy: validation
Validate before calling
// Pre-check every subcommand against the token's scope before sending.
const scopes = new Set(tokenInfo.scopes);
const disallowed = chainCommands.filter(c => !scopes.has(canonicalizeCommand(c)));
if (disallowed.length && tokenInfo.clientId !== 'root') {
throw new Error(`Subcommands out of scope: ${disallowed.join(', ')}`);
} Type guard
const isInScope = (token: TokenInfo | undefined, name: string): boolean => !token || token.clientId === 'root' || token.scopes.includes(name);
Try / catch
try { await browse.chain(payload, { tokenInfo }); }
catch (err) {
if (/Chain rejected/.test(err.message)) {
// parse the listed scopes, filter the chain, and retry
}
} Prevention
- Keep a manifest of which commands each token scope permits and validate client-side.
- Canonicalize subcommand names before scope checks.
- Issue tokens with the minimal scope set the chain actually needs.
When it happens
Trigger: A scoped API token (clientId !== 'root') issues `browse chain '[["click","@e5"]]` where `click` is not listed in the token's scopes (line 633). The check runs before any subcommand executes.
Common situations: A read-only token attempting a write subcommand (click/type/fill) inside a chain; an alias the user thought was in scope but was canonicalized to a different name; multi-tenant deployments where scopes differ per client.
Related errors
- tab-each rejected: subcommand "${innerRaw}" not allowed by y
- Invalid skill name "${name}". Must be lowercase letters/digi
- Invalid file path in stageSkill: "${relPath}".
- commitSkill: staged dir "${opts.stagedDir}" is a symlink — r
- commitSkill: destination "${dest}" escapes tier root.
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/d63f28c831b70ffe.
Report an issue: GitHub.