feat: auth0 audit claude skill#93
Conversation
📝 WalkthroughWalkthroughAdds an Auth0 security-audit skill for Claude Code: MCP wrapper and SessionStart hook, passkeys readiness checker, TokenGraveyard M2M audit, findings renderer, demo-tenant teardown, package/config, and documentation. ChangesAuth0 Security Audit Plugin
Sequence Diagram(s)sequenceDiagram
participant Claude as Claude Code
participant Wrapper as auth0-mcp-wrapper
participant Keychain as macOS Keychain
participant MCP as `@auth0/auth0-mcp-server`
participant Hook as inject-auth0-context
Claude->>Wrapper: start MCP server
Wrapper->>Keychain: check auth0-mcp token
alt token missing
Wrapper->>MCP: run init (device auth flow)
MCP-->>Keychain: token saved
end
Wrapper->>MCP: spawn run
Claude->>Hook: SessionStart
Hook->>Hook: read AUTH0_DOMAIN/.claude creds
Hook-->>Claude: inject tenant context + tool list
sequenceDiagram
participant CLI as TokenGraveyard CLI
participant API as Auth0 Management API
participant Scorer as BlastRadius Scorer
participant Renderer as Report Renderer
CLI->>API: fetch non_interactive apps (paginated)
API-->>CLI: app list
CLI->>API: fetch grants per app
API-->>CLI: grants with scopes
CLI->>API: query last SCE event per app
API-->>CLI: last-used timestamps
CLI->>Scorer: compute blast radius & tier
Scorer-->>Renderer: per-app findings
Renderer-->>CLI: write markdown + JSON outputs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plugins/auth0/skills/auth0-audit/.gitignore`:
- Line 5: Remove the ".claude" entry from .gitignore so the shared skill
artifacts under .claude (e.g. .claude/commands/auth0-audit.md and
.claude/settings.json) are committed; ensure team-required files like
.claude/settings.json remain tracked and do not get globally ignored, and if
there are local-only secrets, replace the broad ignore with specific patterns
(not the entire .claude directory) so hooks/mcp-server.js and auth0-audit skill
files can be checked into the repo.
In `@plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js`:
- Around line 43-46: The DRY_RUN branch in apiFetch currently returns an empty
array for every method so preview mode never discovers resources; change it to
only short-circuit destructive calls: inside the apiFetch function (check the
DRY_RUN, method and url variables), if DRY_RUN is true and method is a
destructive verb (at minimum "DELETE", and optionally "POST"/"PUT"/"PATCH" if
appropriate), log `[DRY RUN] ${method} ${url}` and return [] but allow "GET"
(and other non-destructive methods) to proceed to the real fetch so preview can
list resources; update the DRY_RUN checks at the other locations referenced (the
blocks around lines with the same pattern) to follow the same rule.
- Around line 66-68: The script only fetches the first page (per_page=100) from
list endpoints; update the apiFetch calls that populate actions and M2M apps to
paginate until no more results. For the actions endpoint (the apiFetch('GET',
'/api/v2/actions/actions?per_page=100') call that feeds the actions and
demoActions variables) implement a loop that requests successive pages (e.g.,
page=0,1,2 or page=1,2,3 depending on API), accumulating results into the
actions array and stopping when a page returns an empty array or fewer than
per_page items; do the same for the client/M2M apps endpoint used around lines
84-86 so you collect all clients before filtering demo apps. Only proceed to
deletion and print the completion message after all pages have been fetched and
processed.
In `@plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js`:
- Around line 52-58: The fetch attempts in apiFetch lack a per-request timeout
causing possible hangs; modify the retry loop around the fetch call (the block
that creates `res = await fetch(url, {...})` inside `apiFetch`) to use an
AbortController per attempt, pass its signal to fetch, and set a timer (e.g.,
TIMEOUT_MS) that calls controller.abort() after the timeout; ensure you clear
the timer after fetch completes or on error so resources don’t leak, and handle
the abort error as a retriable failure (so existing 429 retry logic still
applies).
- Around line 83-89: The current Promise.all call swallows API failures by using
.catch(() => null/[]), which conflates "API access error" with "feature not
configured"; remove those fallback catches on the apiFetch calls (prompts,
guardianFactors, customDomains, clients) and instead let errors propagate or
catch them once around the Promise.all so you can distinguish API errors from
missing config—e.g., call Promise.all([apiFetch(...), ...]) without per-call
.catch and wrap that Promise.all in a try/catch that sets an explicit apiError
flag or throws, then set data = { prompts, guardianFactors, customDomains,
clients } only on success and handle apiError separately to produce appropriate
remediation messaging.
In `@plugins/auth0/skills/auth0-audit/tools/token-graveyard.js`:
- Around line 145-146: The tier() function currently treats a null result from
fetchLastSceDate() as if the credential was "never used" and returns
'revoke_now'; change tier() to explicitly detect a null daysSinceLast and return
an 'unknown' remediation (or trigger the fallback scan mentioned in the comment)
instead of 'revoke_now', and only apply the strict revoke/rotate logic when
daysSinceLast is a non-null number; ensure fetchLastSceDate(), tier(), and any
callers handle the 'unknown' sentinel consistently so missing/unsupported
telemetry doesn't escalate to the strongest remediation.
- Around line 22-24: The parsed dormantThreshold (thresholdArg /
dormantThreshold) is only used in the header but not in the scoring logic;
change the tiering and summary logic to derive thresholds from dormantThreshold
instead of hardcoding 30/60/90 and “>60”. Update the tier() function to use
boundaries dormantThreshold, dormantThreshold*2, dormantThreshold*3 (replace the
hardcoded 30/60/90 checks) and update the summary output/condition that
currently checks “>60” to use dormantThreshold*2 (or the appropriate derived
value), and ensure any other checks that reference the hardcoded 60/90 values
(e.g. near symbols tier, summary) are replaced to reference dormantThreshold so
the --dormant-threshold flag actually controls scoring and recommendations.
- Around line 77-81: The code assumes grant.scope is a space-delimited string
and calls .split(' '), but Auth0 returns scope as an array; change the logic in
token-graveyard.js around grantList handling and the updatedScopes computation
to normalize scope into an array first (e.g., if Array.isArray(grant.scope) use
it, otherwise split string defensively), then deduplicate and ensure the final
payload sent to the PATCH `/api/v2/client-grants/{id}` uses an array for scope;
update both places that compute updatedScopes (the block creating updatedScopes
and the later similar usage at line ~251) to use the same normalization helper
logic so scope is always an array before merging and sending.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9d3b6283-4792-42a7-8b0b-6293a4f485e7
📒 Files selected for processing (9)
plugins/auth0/skills/auth0-audit/.gitignoreplugins/auth0/skills/auth0-audit/CLAUDE.mdplugins/auth0/skills/auth0-audit/hooks/inject-auth0-context.jsplugins/auth0/skills/auth0-audit/hooks/mcp-server.jsplugins/auth0/skills/auth0-audit/package.jsonplugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.jsplugins/auth0/skills/auth0-audit/tools/passkeys-readiness.jsplugins/auth0/skills/auth0-audit/tools/render-findings.jsplugins/auth0/skills/auth0-audit/tools/token-graveyard.js
| if (DRY_RUN) { | ||
| console.log(`[DRY RUN] ${method} ${url}`); | ||
| return []; | ||
| } |
There was a problem hiding this comment.
Let --dry-run read state and skip only destructive calls.
Because apiFetch() returns [] for every method, dry-run never discovers any demo resources and always behaves like the tenant is already clean. The preview mode should still execute GETs and no-op only the DELETEs.
Minimal fix
- if (DRY_RUN) {
+ if (DRY_RUN && method !== 'GET') {
console.log(`[DRY RUN] ${method} ${url}`);
return [];
}Also applies to: 66-68, 84-86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js` around
lines 43 - 46, The DRY_RUN branch in apiFetch currently returns an empty array
for every method so preview mode never discovers resources; change it to only
short-circuit destructive calls: inside the apiFetch function (check the
DRY_RUN, method and url variables), if DRY_RUN is true and method is a
destructive verb (at minimum "DELETE", and optionally "POST"/"PUT"/"PATCH" if
appropriate), log `[DRY RUN] ${method} ${url}` and return [] but allow "GET"
(and other non-destructive methods) to proceed to the real fetch so preview can
list resources; update the DRY_RUN checks at the other locations referenced (the
blocks around lines with the same pattern) to follow the same rule.
| const data = await apiFetch('GET', '/api/v2/actions/actions?per_page=100'); | ||
| const actions = Array.isArray(data) ? data : (data.actions ?? []); | ||
| const demoActions = actions.filter(a => a.name?.startsWith('demo-')); |
There was a problem hiding this comment.
Paginate the list endpoints before reporting teardown success.
Both discovery calls stop at the first 100 records. In a larger tenant, demo Actions or M2M apps beyond page 1 are left behind even though the script prints a clean completion message.
Also applies to: 84-86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js` around
lines 66 - 68, The script only fetches the first page (per_page=100) from list
endpoints; update the apiFetch calls that populate actions and M2M apps to
paginate until no more results. For the actions endpoint (the apiFetch('GET',
'/api/v2/actions/actions?per_page=100') call that feeds the actions and
demoActions variables) implement a loop that requests successive pages (e.g.,
page=0,1,2 or page=1,2,3 depending on API), accumulating results into the
actions array and stopping when a page returns an empty array or fewer than
per_page items; do the same for the client/M2M apps endpoint used around lines
84-86 so you collect all clients before filtering demo apps. Only proceed to
deletion and print the completion message after all pages have been fetched and
processed.
| const res = await fetch(url, { | ||
| headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }, | ||
| }); | ||
| if (res.status === 429) { | ||
| const wait = Math.pow(2, attempt) * 500; | ||
| await new Promise(r => setTimeout(r, wait)); | ||
| continue; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether timeout controls exist around fetch in this file.
rg -n "\\bfetch\\(" plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js
rg -n "AbortController|signal\\s*:" plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.jsRepository: auth0/agent-skills
Length of output: 100
🏁 Script executed:
# Get the full context around the fetch call
head -n 80 plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js | tail -n 40Repository: auth0/agent-skills
Length of output: 1444
🏁 Script executed:
# Also check the complete file size to see if we should read more
wc -l plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.jsRepository: auth0/agent-skills
Length of output: 127
🏁 Script executed:
# Search for any timeout-related patterns elsewhere in the file
rg -n "timeout|Timeout|TIMEOUT|AbortController|signal" plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.jsRepository: auth0/agent-skills
Length of output: 116
Add a request timeout to each fetch attempt.
The apiFetch function makes unauthenticated HTTP requests without any timeout mechanism. A stalled connection can cause the CLI to hang indefinitely, even with retry logic, since none of the retry attempts include timeout protection.
✅ Suggested fix
for (let attempt = 0; attempt <= retries; attempt++) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
+ signal: controller.signal,
});
+ clearTimeout(timeoutId);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js` around lines 52
- 58, The fetch attempts in apiFetch lack a per-request timeout causing possible
hangs; modify the retry loop around the fetch call (the block that creates `res
= await fetch(url, {...})` inside `apiFetch`) to use an AbortController per
attempt, pass its signal to fetch, and set a timer (e.g., TIMEOUT_MS) that calls
controller.abort() after the timeout; ensure you clear the timer after fetch
completes or on error so resources don’t leak, and handle the abort error as a
retriable failure (so existing 429 retry logic still applies).
| const [prompts, guardianFactors, customDomains, clients] = await Promise.all([ | ||
| apiFetch('/api/v2/prompts').catch(() => null), | ||
| apiFetch('/api/v2/guardian/factors').catch(() => null), | ||
| apiFetch('/api/v2/custom_domains').catch(() => []), | ||
| apiFetch('/api/v2/clients?app_type=spa,native&fields=client_id,name,app_type&per_page=100').catch(() => []), | ||
| ]); | ||
| data = { prompts, guardianFactors, customDomains, clients }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify catch-based masking of API errors in this tool.
rg -n "apiFetch\\('/api/v2/.*'\\)\\.catch\\(" plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.jsRepository: auth0/agent-skills
Length of output: 362
🏁 Script executed:
cat -n plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.jsRepository: auth0/agent-skills
Length of output: 11573
Don't coerce fetch failures into failed readiness checks.
Swallowing API errors with .catch(() => null/[]) masks authentication, authorization, and network failures as missing configuration. This produces false remediation guidance: users will be prompted to reconfigure their tenant when the actual issue is API access failure.
The checks cannot distinguish between "API error" and "feature not configured"—both produce the same failed checkpoint output with incorrect remediation steps.
✅ Suggested fix
- const [prompts, guardianFactors, customDomains, clients] = await Promise.all([
- apiFetch('/api/v2/prompts').catch(() => null),
- apiFetch('/api/v2/guardian/factors').catch(() => null),
- apiFetch('/api/v2/custom_domains').catch(() => []),
- apiFetch('/api/v2/clients?app_type=spa,native&fields=client_id,name,app_type&per_page=100').catch(() => []),
- ]);
+ const [prompts, guardianFactors, customDomains, clients] = await Promise.all([
+ apiFetch('/api/v2/prompts'),
+ apiFetch('/api/v2/guardian/factors'),
+ apiFetch('/api/v2/custom_domains'),
+ apiFetch('/api/v2/clients?app_type=spa,native&fields=client_id,name,app_type&per_page=100'),
+ ]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js` around lines 83
- 89, The current Promise.all call swallows API failures by using .catch(() =>
null/[]), which conflates "API access error" with "feature not configured";
remove those fallback catches on the apiFetch calls (prompts, guardianFactors,
customDomains, clients) and instead let errors propagate or catch them once
around the Promise.all so you can distinguish API errors from missing
config—e.g., call Promise.all([apiFetch(...), ...]) without per-call .catch and
wrap that Promise.all in a try/catch that sets an explicit apiError flag or
throws, then set data = { prompts, guardianFactors, customDomains, clients }
only on success and handle apiError separately to produce appropriate
remediation messaging.
| const thresholdArg = args.find(a => a.startsWith('--dormant-threshold=')); | ||
| const dormantThreshold = thresholdArg ? parseInt(thresholdArg.split('=')[1]) : 60; | ||
|
|
There was a problem hiding this comment.
Wire --dormant-threshold into the actual scoring logic.
The flag is parsed and printed, but the audit still hardcodes 30/60/90 in tier() and >60 in the summary. Right now --dormant-threshold=30 or 90 changes the report header only, not the recommendations.
Also applies to: 144-149, 346-347
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/tools/token-graveyard.js` around lines 22 -
24, The parsed dormantThreshold (thresholdArg / dormantThreshold) is only used
in the header but not in the scoring logic; change the tiering and summary logic
to derive thresholds from dormantThreshold instead of hardcoding 30/60/90 and
“>60”. Update the tier() function to use boundaries dormantThreshold,
dormantThreshold*2, dormantThreshold*3 (replace the hardcoded 30/60/90 checks)
and update the summary output/condition that currently checks “>60” to use
dormantThreshold*2 (or the appropriate derived value), and ensure any other
checks that reference the hardcoded 60/90 values (e.g. near symbols tier,
summary) are replaced to reference dormantThreshold so the --dormant-threshold
flag actually controls scoring and recommendations.
| const grants = await apiFetch(`/api/v2/client-grants?client_id=${clientId}&audience=https://${DOMAIN}/api/v2`); | ||
| const grantList = Array.isArray(grants) ? grants : (grants.client_grants ?? []); | ||
| if (grantList.length > 0) { | ||
| const grant = grantList[0]; | ||
| const updatedScopes = [...new Set([...(grant.scope || '').split(' ').filter(Boolean), 'delete:client_grants'])]; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Auth0 Management API /api/v2/client-grants response schema: is the scopefield an array of strings or a space-delimited string? Also, what payload shape does PATCH /api/v2/client-grants/{id} expect forscope?
💡 Result:
In the Auth0 Management API, the scope field for client grants is an array of strings, not a space-delimited string [1][2][3]. For the PATCH /api/v2/client-grants/{id} endpoint, the scope field in the request payload also expects an array of strings [2]. The schema for the update request defines scope as an array of strings (or null) [2]. Example payload for updating the scope: { "scope": ["read:example", "write:example"] } [1][4]
Citations:
- 1: https://auth0.com/docs/get-started/applications/application-access-to-apis-client-grants
- 2: https://auth0.com/docs/api/management/v2/client-grants/patch-client-grants-by-id
- 3: https://auth0.com/docs/api/management/v2/client-grants/post-client-grants
- 4: https://www.educative.io/courses/control-access-to-your-apps-using-auth0-api-in-javascript/updating-and-deleting-client-grant
🏁 Script executed:
cat -n plugins/auth0/skills/auth0-audit/tools/token-graveyard.js | sed -n '245,255p'Repository: auth0/agent-skills
Length of output: 511
Normalize grant scopes—Auth0 returns scope as an array, not a string.
Both line 81 and line 251 call .split(' ') assuming scope is a space-delimited string. However, the Auth0 Management API returns scope as an array of strings. This will throw "split is not a function" at runtime. Additionally, the PATCH endpoint for /api/v2/client-grants/{id} expects scope to be an array, not a string.
Add a normalization helper to handle both formats defensively:
Suggested fix
+function normalizeScopes(scope) {
+ if (Array.isArray(scope)) return scope.filter(Boolean);
+ if (typeof scope === 'string') return scope.split(' ').filter(Boolean);
+ return [];
+}
+
async function ensureDeleteClientGrantsScope() {
const payload = decodeJwtPayload(TOKEN);
if (!payload) return; // can't inspect — proceed and let the API fail naturally
@@
- const updatedScopes = [...new Set([...(grant.scope || '').split(' ').filter(Boolean), 'delete:client_grants'])];
+ const updatedScopes = [...new Set([...normalizeScopes(grant.scope), 'delete:client_grants'])];
@@
- const allScopes = grants.flatMap(g => g.scope ? g.scope.split(' ') : []);
+ const allScopes = grants.flatMap(g => normalizeScopes(g.scope));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const grants = await apiFetch(`/api/v2/client-grants?client_id=${clientId}&audience=https://${DOMAIN}/api/v2`); | |
| const grantList = Array.isArray(grants) ? grants : (grants.client_grants ?? []); | |
| if (grantList.length > 0) { | |
| const grant = grantList[0]; | |
| const updatedScopes = [...new Set([...(grant.scope || '').split(' ').filter(Boolean), 'delete:client_grants'])]; | |
| function normalizeScopes(scope) { | |
| if (Array.isArray(scope)) return scope.filter(Boolean); | |
| if (typeof scope === 'string') return scope.split(' ').filter(Boolean); | |
| return []; | |
| } | |
| const grants = await apiFetch(`/api/v2/client-grants?client_id=${clientId}&audience=https://${DOMAIN}/api/v2`); | |
| const grantList = Array.isArray(grants) ? grants : (grants.client_grants ?? []); | |
| if (grantList.length > 0) { | |
| const grant = grantList[0]; | |
| const updatedScopes = [...new Set([...normalizeScopes(grant.scope), 'delete:client_grants'])]; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/tools/token-graveyard.js` around lines 77 -
81, The code assumes grant.scope is a space-delimited string and calls .split('
'), but Auth0 returns scope as an array; change the logic in token-graveyard.js
around grantList handling and the updatedScopes computation to normalize scope
into an array first (e.g., if Array.isArray(grant.scope) use it, otherwise split
string defensively), then deduplicate and ensure the final payload sent to the
PATCH `/api/v2/client-grants/{id}` uses an array for scope; update both places
that compute updatedScopes (the block creating updatedScopes and the later
similar usage at line ~251) to use the same normalization helper logic so scope
is always an array before merging and sending.
| if (daysSinceLast === null || (daysSinceLast > 90 && blastScore > 5)) return 'revoke_now'; | ||
| if (daysSinceLast > 60 || blastScore > 8) return 'rotate_7d'; |
There was a problem hiding this comment.
Don’t equate log lookup failures with “never used.”
fetchLastSceDate() returns null for unsupported queries and transient API failures, and tier() maps that straight to revoke_now. That turns missing telemetry into the strongest remediation recommendation. Keep an explicit unknown state or implement the fallback scan mentioned in the comment.
Also applies to: 192-203
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/tools/token-graveyard.js` around lines 145 -
146, The tier() function currently treats a null result from fetchLastSceDate()
as if the credential was "never used" and returns 'revoke_now'; change tier() to
explicitly detect a null daysSinceLast and return an 'unknown' remediation (or
trigger the fallback scan mentioned in the comment) instead of 'revoke_now', and
only apply the strict revoke/rotate logic when daysSinceLast is a non-null
number; ensure fetchLastSceDate(), tier(), and any callers handle the 'unknown'
sentinel consistently so missing/unsupported telemetry doesn't escalate to the
strongest remediation.
❌ skillsaw — Issues foundFull report
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plugins/auth0/skills/auth0-audit/.claude/commands/auth0-audit.md`:
- Around line 4-6: The markdown contains unlabeled fenced code blocks (for
example the snippet starting with "/auth0-audit [--tool
actions|tokens|passkeys|all]...") which trigger MD040; update each fenced block
to include an appropriate language identifier (e.g., ```bash for CLI examples,
```text for plain text, ```json for JSON responses) so the blocks at the
mentioned occurrences are explicitly labeled; search for all triple-backtick
blocks in the document (including the ones around the CLI usage and the regions
covering the other reported ranges) and add the correct language tag to each
opening fence.
- Around line 20-25: Replace the manual recovery instructions that tell users to
run `npx `@auth0/auth0-mcp-server` init` with guidance that credential setup is
automated by the plugin hooks; specifically update the auth0-audit message block
in auth0-audit.md to remove the manual init command and instead say something
like "Credential setup is managed automatically by the skill hooks. Please
ensure the Auth0 audit skill hooks are active, then re-run `/auth0-audit`." Also
verify references to the automated flow in mcp-server.js / the device
authorization flow and ensure no other docs or messages instruct a manual
`auth0-mcp-server init`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cc09ff3f-c40d-47dc-8b71-d32787914486
📒 Files selected for processing (3)
plugins/auth0/skills/auth0-audit/.claude/commands/auth0-audit.mdplugins/auth0/skills/auth0-audit/.claude/settings.jsonplugins/auth0/skills/auth0-audit/.gitignore
✅ Files skipped from review due to trivial changes (2)
- plugins/auth0/skills/auth0-audit/.gitignore
- plugins/auth0/skills/auth0-audit/.claude/settings.json
| ``` | ||
| /auth0-audit [--tool actions|tokens|passkeys|all] [--tenant DOMAIN] [--cache] | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced code blocks (MD040).
The unlabeled fenced blocks trigger markdownlint warnings; please mark each with an explicit language (text, bash, json, markdown, etc.) to keep docs lint-clean.
Also applies to: 23-25, 63-118, 125-128, 147-150, 176-179, 200-203
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 4-4: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/.claude/commands/auth0-audit.md` around
lines 4 - 6, The markdown contains unlabeled fenced code blocks (for example the
snippet starting with "/auth0-audit [--tool actions|tokens|passkeys|all]...")
which trigger MD040; update each fenced block to include an appropriate language
identifier (e.g., ```bash for CLI examples, ```text for plain text, ```json for
JSON responses) so the blocks at the mentioned occurrences are explicitly
labeled; search for all triple-backtick blocks in the document (including the
ones around the CLI usage and the regions covering the other reported ranges)
and add the correct language tag to each opening fence.
| > The auth0-mcp-server is not connected or not authenticated. | ||
| > Please run the following command in your terminal, then re-run `/auth0-audit`: | ||
| > | ||
| > ``` | ||
| > npx @auth0/auth0-mcp-server init | ||
| > ``` |
There was a problem hiding this comment.
Remove manual auth0-mcp-server init recovery instructions.
Line 20–25 currently directs users to a manual init flow that conflicts with this plugin’s automated credential handling path and can send users down the wrong remediation route.
Suggested doc fix
- > Please run the following command in your terminal, then re-run `/auth0-audit`:
- >
- > ```
- > npx `@auth0/auth0-mcp-server` init
- > ```
+ > Credential setup is managed automatically by the skill hooks.
+ > Please ensure the Auth0 audit skill hooks are active, then re-run `/auth0-audit`.Based on learnings: "Do NOT attempt to run auth0-mcp-server init manually; credential handling is automated via hooks/mcp-server.js using device authorization flow".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| > The auth0-mcp-server is not connected or not authenticated. | |
| > Please run the following command in your terminal, then re-run `/auth0-audit`: | |
| > | |
| > ``` | |
| > npx @auth0/auth0-mcp-server init | |
| > ``` | |
| > The auth0-mcp-server is not connected or not authenticated. | |
| > Credential setup is managed automatically by the skill hooks. | |
| > Please ensure the Auth0 audit skill hooks are active, then re-run `/auth0-audit`. |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 23-23: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/auth0/skills/auth0-audit/.claude/commands/auth0-audit.md` around
lines 20 - 25, Replace the manual recovery instructions that tell users to run
`npx `@auth0/auth0-mcp-server` init` with guidance that credential setup is
automated by the plugin hooks; specifically update the auth0-audit message block
in auth0-audit.md to remove the manual init command and instead say something
like "Credential setup is managed automatically by the skill hooks. Please
ensure the Auth0 audit skill hooks are active, then re-run `/auth0-audit`." Also
verify references to the automated flow in mcp-server.js / the device
authorization flow and ensure no other docs or messages instruct a manual
`auth0-mcp-server init`.
|
test |
This PR introduces the Auth0 Security Audit Plugin (/auth0-audit) for Claude Code — a slash command skill that performs a structured
security audit of an Auth0 Customer Identity Cloud (CIC) tenant and produces Markdown receipts and structured JSON findings.
The plugin ships three independent audit tools, orchestrated by a single /auth0-audit command:
ActionsWhisperer — analyzes all deployed Auth0 Actions for security anti-patterns: hardcoded secrets, PII persistence to user_metadata,
external claim injection, SSRF-prone HTTP calls, and unallowlisted service calls. Produces actions-audit.md and actions-findings.json
grouped by pipeline trigger.
TokenGraveyard — identifies dormant and overprivileged M2M (non-interactive) applications by querying sce log events per client, computing
a blast-radius score from the granted Management API scopes, and assigning apps to tiers (Revoke Now / Rotate Within 7 Days / Scope-Narrow
/ Monitor). Revocations are approval-gated — the user must explicitly confirm each delete_client_grant before it executes.
PasskeysReadiness — runs a 4-checkpoint assessment of whether the tenant meets the prerequisites for enabling Passkeys (WebAuthn): New
Universal Login experience, WebAuthn Guardian factors enabled, verified custom domain, and at least one SPA or native app registered.
Outputs a scored report with a step-by-step remediation sequence.
Additional components:
apps for reliable demo and offline playback via --cache
The plugin targets Auth0 CIC only and does not support Okta Workforce Identity Cloud or Okta Identity Engine.
▎⚠️ Known limitation: @auth0/auth0-mcp-server must be installed and initialized manually from within the skill directory (npx
▎ @auth0/auth0-mcp-server init). Global installation and automated delivery as part of the skill setup are not yet integrated. This step is
▎ a one-time prerequisite before the first /auth0-audit run.
References
Testing
Prerequisites:
▎ @auth0/auth0-mcp-server must be run manually from the skill directory before testing. Global install and delivery are not yet integrated
▎ — run npx @auth0/auth0-mcp-server init from plugins/auth0/skills/auth0-audit/ to complete the device authorization flow and persist the
▎ keychain token. Subsequent runs are handled automatically by hooks/mcp-server.js.
Automated (offline demo mode):
Live tenant testing:
Environments tested: Node.js 22.x on macOS (darwin 25.4), Auth0 CIC tenant (AU region).
Checklist