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.
Ask: Please expose this via GraphQL, on all plans. Scoped only to Store Admins would be fine.
Current Workaround
- Browse to
admin.shopify.com/store/{store-handle}/settings/organization-account/roles/{the-role-id} - Edge > Inspect > Console > run the script below
- 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.]
