jackwener/OpenCLI · error

Command ${key} must declare access: 'read' | 'write'

Error message

Command ${key} must declare access: 'read' | 'write'

What it means

assertCommandAccess validates every RawCliCommand registered via registerCommand/normalizeCommand: each command must explicitly declare access as 'read' or 'write'. The check exists so the CLI can enforce permission semantics (read-only vs mutating operations) per command; an undeclared access is treated as a registration-time programming error.

Source

Thrown at src/registry.ts:211

    if (strategy === Strategy.COOKIE && cmd.domain) {
      navigateBefore = `https://${cmd.domain}`;
    } else if (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL) {
      // Non-PUBLIC without domain: needs authenticated browser context
      // but no specific pre-navigation URL. `true` signals this to
      // shouldUseBrowserSession without triggering resolvePreNav.
      navigateBefore = true;
    }
  }

  return browser
    ? { ...cmd, strategy, browser: true, navigateBefore } as BrowserCliCommand
    : { ...cmd, strategy, browser: false, navigateBefore } as NonBrowserCliCommand;
}

function assertCommandAccess(cmd: Pick<RawCliCommand, 'site' | 'name'> & { access?: unknown }): asserts cmd is RawCliCommand {
  if (cmd.access === 'read' || cmd.access === 'write') return;
  const key = `${cmd.site}/${cmd.name}`;
  throw new Error(`Command ${key} must declare access: 'read' | 'write'`);
}

function assertSiteSession(cmd: Pick<RawCliCommand, 'site' | 'name'> & { siteSession?: unknown }): void {
  if (cmd.siteSession === undefined) return;
  const key = `${cmd.site}/${cmd.name}`;
  if (cmd.siteSession !== 'ephemeral' && cmd.siteSession !== 'persistent') {
    throw new Error(`Command ${key} siteSession must be one of: ephemeral, persistent`);
  }
}

export function registerCommand(cmd: RawCliCommand): void {
  const normalized = normalizeCommand(cmd);
  const canonicalKey = fullName(normalized);
  const existing = _registry.get(canonicalKey);
  if (existing?.aliases) {
    for (const alias of existing.aliases) {
      _registry.delete(`${existing.site}/${alias}`);
    }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Add access: 'read' or access: 'write' to the command definition before registering it.
  2. Choose 'write' for any command that mutates site state, 'read' otherwise.
  3. If commands come from a config/table, add a pre-registration check that every entry has a valid access value.
  4. Fix casing/value typos — only the exact strings 'read' and 'write' pass.

Example fix

// before
registerCommand({ site: "github", name: "star-repo", run: fn });
// after
registerCommand({ site: "github", name: "star-repo", access: "write", run: fn });
Defensive patterns

Strategy: type-guard

Validate before calling

type Access = "read" | "write";
function hasValidAccess(cmd: { access?: unknown }): cmd is { access: Access } & Record<string, unknown> {
  return cmd.access === "read" || cmd.access === "write";
}
if (!hasValidAccess(cmd)) throw new Error(`Command ${cmd.site}/${cmd.name} needs access`);
registerCommand(cmd);

Type guard

function declaresAccess(cmd: { access?: unknown }): cmd is { access: "read" | "write" } {
  return cmd.access === "read" || cmd.access === "write";
}

Try / catch

try {
  registerCommand(cmd);
} catch (e) {
  if (e instanceof Error && e.message.includes("must declare access")) {
    console.error(`Fix command definition: ${e.message}`);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling registerCommand(cmd) (directly or through a registry/bulk-registration path) with a command object whose access property is missing, undefined, or set to any value other than 'read' or 'write' (e.g. 'rw', true, null).

Common situations: Adding a new command definition and forgetting the access field; copying a command object and dropping the property; data-driven command tables where one row lacks access; typo like 'Read' (case-sensitive).

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/da556a399bce9d57. Report an issue: GitHub.