πŸ€– Let merchants read role/collaborator permissions via GraphQL (and Sidekick)

The Problem & Ask

I am not a developer β€” I am a new merchant to Shopify.

There is currently no way to programmatically retrieve either (a) the permissions a role grants, or (b) which role a staff member or collaborator has been assigned.

Use case: security audit of collaborator access. Today this means manually opening each role page in the Shopify admin and visually checking permission checkboxes. I also cannot ask the Shopify AI Assistant to explain the risk of the permissions granted to a role/collaborator, as it has no access to them either.

:green_circle: Ask: Please expose this via GraphQL, on all plans. Scoped only to Store Admins would be fine.

Current Workaround

  1. Browse to admin.shopify.com/store/{store-handle}/settings/organization-account/roles/{the-role-id}
  2. Edge > Inspect > Console > run the script below
  3. Give this to the Shopify AI Assistant using a 2-step prompt

The real value is that Sidekick can then be asked about these permissions. Without this workaround, I would have missed some alarmingly broad permissions I had naively granted (as requested) to a Shopify Partner based abroad.


Image: What Sidekick found and helped with


Appendix

Two-step Sidekick Prompt

Done in two steps as one prompt was too long for Sidekick.

First:

Hello Sidekick.
Please reply OKAY for now. I will reference this in the next message:

<collaborator_permissions_audit>
[output from Edge > Inspect > Console > run script]
</collaborator_permissions_audit>

Second:

# TASK: Security review of a Shopify Collaborator's permissions

## My situation

I am the **store owner** and I am **non-technical**. I have:

1. **Granted a collaborator account** to a Shopify Partner agency called `XXXX XXXX` so they can develop my store. The full audit of permissions I granted to this collaborator is in `<collaborator_permissions_audit>` (previous message).

2. **Created a custom app** called `claude-code-go-go` with these access scopes: <app_scopes>
read_files,write_files,write_inventory,read_inventory,read_locations,read_products,write_products,read_publications,write_publications</app_scopes>

## Questions

Please answer each question separately. Cite the specific permission line(s) from the audit when relevant.

### Q1 β€” Risk in the granted collaborator permissions

Reviewing `<collaborator_permissions_audit>`:

- Which **granted** permissions (`- [x]`) carry meaningful risk to the store, the business, or me personally (e.g. financial, data exfiltration, irreversible destructive actions, ability to escalate their own access)?
- Of those, which are **standard and necessary** for a theme/store-development engagement, versus **unusual or excessive** for that scope of work?
- Rank findings as **High / Medium / Low** risk and explain *why*, in plain English (no jargon).

### Q2 β€” The collaborator's relationship to my custom app `claude-code-go-go`

A. Does the collaborator's role (as defined in `<collaborator_permissions_audit>`) automatically give them access to the `claude-code-go-go` app's data or its access tokens? Specifically:
   - Can they install, configure, or read the credentials of the app from inside Shopify Admin?
   - Do they inherit any of the app's access scopes (e.g. `write_products`) by virtue of being a collaborator?

B. Do any of the access scopes I granted to `claude-code-go-go` (`<app_scopes>`) go beyond what is already granted in `<collaborator_permissions_audit>`? For the app itself, is it worth worrying about β€” and what's the worst-case scenario if its API token were ever leaked? Or does it require authentication anyway?

## Output format

- Lead with a 3-line **TL;DR** verdict.
- Then answer Q1, then Q2A, then Q2B as separate sections.
- Use plain English. Define any Shopify-specific term the first time you use it.

Script (to get permissions)

Edge > Inspect > Console > run the script below

(() => {
  const deep = [];
  const walk = (root) => {
    if (!root?.querySelectorAll) return;
    root.querySelectorAll('*').forEach(el => {
      deep.push(el);
      if (el.shadowRoot) walk(el.shadowRoot);
    });
  };
  walk(document);

  const parentAcross = (el) => el.parentElement || el.getRootNode()?.host || null;

  const hasLegacyCardFirstSectionAncestor = (el) => {
    for (let cur = el; cur; cur = parentAcross(cur)) {
      if (cur.classList?.contains?.('Polaris-LegacyCard__FirstSectionPadding')) return true;
    }
    return false;
  };

  const nearestPolarisBoxPadding = (el) => {
    for (let cur = parentAcross(el); cur; cur = parentAcross(cur)) {
      if (cur.classList?.contains?.('Polaris-Box')) {
        const v = parseFloat(getComputedStyle(cur).paddingInlineStart);
        return Number.isFinite(v) ? v : 0;
      }
    }
    return 0;
  };

  const normaliseLabel = (s) => s.replace('(Including ', '(including ');

  const legacyHeadings = new Set(
    deep.filter(el =>
      el.tagName === 'S-INTERNAL-HEADING' && hasLegacyCardFirstSectionAncestor(el)
    )
  );

  const byCat = new Map();
  const rows = [];
  let category = null;
  let subgroup = null;

  const ensureGroup = () => {
    if (!category) return null;
    if (!byCat.has(category)) byCat.set(category, new Map());
    const subs = byCat.get(category);
    const key = subgroup || '_';
    if (!subs.has(key)) subs.set(key, { rows: [] });
    return subs.get(key);
  };

  deep.forEach(el => {
    if (legacyHeadings.has(el)) {
      category = el.textContent.trim();
      subgroup = null;
      return;
    }
    if (el.tagName === 'S-INTERNAL-PARAGRAPH' && el.getAttribute('fontweight') === 'medium') {
      const txt = el.textContent.trim();
      if (category && txt && txt !== category) subgroup = txt;
      return;
    }
    if (el.tagName !== 'S-INTERNAL-CHECKBOX') return;

    const label = el.getAttribute('label') || '';
    const headerMatch = label.match(/^(?:Select|Deselect) all permissions in (.+)$/);
    if (headerMatch) { category = headerMatch[1]; subgroup = null; return; }
    if (/^(?:Select|Deselect) all permissions$/.test(label)) return;

    const group = ensureGroup();
    if (!group) return;

    const row = {
      category,
      subgroup,
      label: normaliseLabel(label),
      granted: !!el.checked,
      paddingPx: nearestPolarisBoxPadding(el),
    };
    group.rows.push(row);
    rows.push(row);
  });

  const lines = ['## Permissions Breakdown', ''];
  let totalGranted = 0;
  let totalRows = 0;

  for (const [cat, subs] of byCat) {
    let catGranted = 0;
    let catTotal = 0;
    for (const { rows: r } of subs.values()) {
      catTotal += r.length;
      catGranted += r.filter(x => x.granted).length;
    }
    totalGranted += catGranted;
    totalRows += catTotal;

    lines.push(`### ${cat} (${catGranted}/${catTotal})`, '');

    for (const [key, { rows: r }] of subs) {
      if (key !== '_') lines.push(`#### ${key}`, '');
      const stack = [];
      for (const row of r) {
        while (stack.length && stack[stack.length - 1] >= row.paddingPx) stack.pop();
        lines.push(`${'  '.repeat(stack.length)}- [${row.granted ? 'x' : ' '}] ${row.label}`);
        stack.push(row.paddingPx);
      }
      lines.push('');
    }
  }

  const md = lines.join('\n').replace(/\n+$/, '\n');

  console.table(rows.map(({ category, subgroup, label, granted, paddingPx }) =>
    ({ category, subgroup, label, granted, paddingPx })));
  console.log(md);
  window.__roleAudit = { rows, byCat, md, totals: { granted: totalGranted, total: totalRows } };

  try {
    copy(md);
    console.log('%cβœ“ Markdown copied to clipboard', 'color:green;font-weight:bold');
  } catch (e) {
    console.warn('clipboard copy failed β€” run: copy(window.__roleAudit.md)');
  }

  return { categories: byCat.size, total: totalRows, granted: totalGranted };
})();

The script outputs:

## Permissions Breakdown

### Home (1/1)

- [x] Home

### Orders (17/19)

- [x] View
- [x] Manage order information
- [x] Edit orders
  - [x] Apply discounts
- [x] Set payment terms
- [x] Charge credit card
- [x] Charge vaulted payment method
- [x] Record payments
- [x] Capture payments
- [x] Fulfill and ship
  - [x] Buy shipping labels
- [x] Cancel
- [x] Export
- [x] Delete

#### Returns and refunds

- [x] Return
- [x] Refund to original payment
  - [ ] Over-refund orders previously refunded to store credit
- [ ] Refund to store credit

[Etc.]

Hi @Michelle,

I appreciate your detailed feature request and workaround for anyone looking to do the same!

I can confirm that at this time there is no public API fields, objects, or queries that can be used to get a store staff members roles or permissions, including collaborator accounts.

We did used to have the StaffMember.privateData.permissions field exposed on the Admin API, however that has been deprecated due to an overhaul with how the permission system works, with the new role based permissions, and there is currently no replacement on the API for this.

Our developers are aware of this limitation and will be looking into potentially adding this on the Admin API in the future, though I can’t provide any guarantees or timelines as to when, but I do recommend subscribing to our Shopify.dev Changelog to be notified if it is added in the future.