Skip to content

feat: auth0 audit claude skill#93

Open
KartikJha wants to merge 2 commits into
mainfrom
hackapalooza-auth0-audit
Open

feat: auth0 audit claude skill#93
KartikJha wants to merge 2 commits into
mainfrom
hackapalooza-auth0-audit

Conversation

@KartikJha

@KartikJha KartikJha commented May 16, 2026

Copy link
Copy Markdown

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:

  • hooks/mcp-server.js — MCP server wrapper that handles keychain token caching and device authorization flow for @auth0/auth0-mcp-server
  • hooks/inject-auth0-context.js — SessionStart hook that surfaces tenant context and available tool usage at session start
  • scripts/setup-demo-tenant.js / teardown-demo-tenant.js — idempotently populates/tears down a dev tenant with synthetic Actions and M2M
    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

  • Auth0 Actions documentation
  • Auth0 Management API — Client Grants
  • Auth0 Passkeys / WebAuthn prerequisites
  • Auth0 MCP Server (@auth0/auth0-mcp-server)

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):

  1. Run node scripts/setup-demo-tenant.js against a dev tenant to populate it and write scripts/demo-cache.json
  2. Run /auth0-audit --cache — this exercises all three tools using cached API responses without live API calls

Live tenant testing:

  1. From the skill directory, run npx @auth0/auth0-mcp-server init once to authenticate and cache the keychain token
  2. Run /auth0-audit (defaults to --tool all)
  3. Verify actions-audit.md, tokens-audit.md, passkeys-readiness.md, and auth0-audit-summary.md are written to the working directory
  4. For TokenGraveyard revocation: confirm the approval prompt appears and no delete_client_grant is executed without explicit y input

Environments tested: Node.js 22.x on macOS (darwin 25.4), Auth0 CIC tenant (AU region).

  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation for new/changed functionality in this PR or in auth0.com/docs
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not the default branch

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Auth0 Security Audit Plugin

Layer / File(s) Summary
Project Setup and Documentation
plugins/auth0/skills/auth0-audit/.gitignore, plugins/auth0/skills/auth0-audit/package.json, plugins/auth0/skills/auth0-audit/CLAUDE.md
Package manifest and scripts, .gitignore for local creds/node artifacts, and CLAUDE.md documenting prerequisites, credential flow (MCP keychain), and plugin layout.
MCP Server Wrapper & SessionStart Hook
plugins/auth0/skills/auth0-audit/hooks/mcp-server.js, plugins/auth0/skills/auth0-audit/hooks/inject-auth0-context.js, plugins/auth0/skills/auth0-audit/.claude/settings.json
Wrapper checks macOS keychain, runs npx @auth0/auth0-mcp-server init if needed, spawns the MCP server with signal forwarding, and a SessionStart hook injects tenant context and available audit tools into Claude system prompts.
Passkeys Readiness Assessment
plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js
Four-check readiness assessment (Universal Login prompt, Guardian WebAuthn, verified custom domains, SPA/native clients), scoring/status, remediation instructions, and renders passkeys-readiness.md (supports --cache).
Token Graveyard M2M Audit
plugins/auth0/skills/auth0-audit/tools/token-graveyard.js
Audits non_interactive M2M apps for blast radius and dormancy, attempts to ensure delete:client_grants scope, gathers grants and last-SCE usage, computes tiers, and writes tokens-audit.md and tokens-findings.json.
Findings Rendering and Demo Cleanup
plugins/auth0/skills/auth0-audit/tools/render-findings.js, plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js
Renderer converts Claude JSON into Markdown/JSON reports for Actions and token receipts. Teardown script deletes demo- Actions and M2M clients with dry-run and retry/backoff handling.
MCP Commands and CLI Docs
plugins/auth0/skills/auth0-audit/.claude/commands/auth0-audit.md
Documents /auth0-audit CLI usage, required MCP connectivity Step 0, tool flows (actions, tokens, passkeys), orchestration order for --tool all, and expected outputs.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • brth31
  • tanya732
  • sanchitmehtagit

Poem

🐰 I hopped through configs, keys, and scripts galore,

Checking passkeys, tokens, and every door,
I scored the blast radius, cleaned the demo slate,
Rendered findings neat for Claude to relate,
A tiny audit rabbit, tidy and secure!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: auth0 audit claude skill' accurately summarizes the main change: adding a new Auth0 audit Claude skill plugin with multiple tools and supporting infrastructure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hackapalooza-auth0-audit

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 975596c and a2cc7a9.

📒 Files selected for processing (9)
  • plugins/auth0/skills/auth0-audit/.gitignore
  • plugins/auth0/skills/auth0-audit/CLAUDE.md
  • plugins/auth0/skills/auth0-audit/hooks/inject-auth0-context.js
  • plugins/auth0/skills/auth0-audit/hooks/mcp-server.js
  • plugins/auth0/skills/auth0-audit/package.json
  • plugins/auth0/skills/auth0-audit/scripts/teardown-demo-tenant.js
  • plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js
  • plugins/auth0/skills/auth0-audit/tools/render-findings.js
  • plugins/auth0/skills/auth0-audit/tools/token-graveyard.js

Comment thread plugins/auth0/skills/auth0-audit/.gitignore Outdated
Comment on lines +43 to +46
if (DRY_RUN) {
console.log(`[DRY RUN] ${method} ${url}`);
return [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +66 to +68
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-'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +52 to +58
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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.js

Repository: 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 40

Repository: 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.js

Repository: 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.js

Repository: 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).

Comment on lines +83 to +89
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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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.js

Repository: auth0/agent-skills

Length of output: 362


🏁 Script executed:

cat -n plugins/auth0/skills/auth0-audit/tools/passkeys-readiness.js

Repository: 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.

Comment on lines +22 to +24
const thresholdArg = args.find(a => a.startsWith('--dormant-threshold='));
const dormantThreshold = thresholdArg ? parseInt(thresholdArg.split('=')[1]) : 60;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +77 to +81
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'])];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 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:


🏁 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.

Suggested change
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.

Comment on lines +145 to +146
if (daysSinceLast === null || (daysSinceLast > 90 && blastScore > 5)) return 'revoke_now';
if (daysSinceLast > 60 || blastScore > 8) return 'rotate_7d';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

@github-actions

Copy link
Copy Markdown

❌ skillsaw — Issues found

Full report
Linting: /home/runner/work/agent-skills/agent-skills


Errors:
  ✗ ERROR [/home/runner/work/agent-skills/agent-skills/plugins/auth0/skills/auth0-audit/CLAUDE.md]: Only SKILL.md is allowed in the skill root directory. Move 'CLAUDE.md' to an appropriate subdirectory such as 'references/' or 'tests/'.

Warnings:
  ⚠ WARNING [/home/runner/work/agent-skills/agent-skills/plugins/auth0/skills/auth0-audit]: Missing SKILL.md

Summary:
  Errors:   1
  Warnings: 1

skillsaw 0.4.3 · config · custom rules · run

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a2cc7a9 and 78d7cd7.

📒 Files selected for processing (3)
  • plugins/auth0/skills/auth0-audit/.claude/commands/auth0-audit.md
  • plugins/auth0/skills/auth0-audit/.claude/settings.json
  • plugins/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

Comment on lines +4 to +6
```
/auth0-audit [--tool actions|tokens|passkeys|all] [--tenant DOMAIN] [--cache]
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +20 to +25
> 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
> ```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
> 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`.

Copy link
Copy Markdown
Contributor

test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants