agentsclimarketplace

Qauthtrailingslash

Skill 3awny/qship/templates/skills/qauthtrailingslash

Ticket → production-PR pipeline for Claude Code & Codex CLI, enforced by hooks. 21-skill pipeline, one configurator, adapts to any codebase.

Install
npx -y skills add 3awny/qship --skill qauthtrailingslash

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Audit API routes for the trailing-slash / 307-redirect auth bug — where a missing or extra trailing slash makes the server issue an HTTP 307 redirect and the client silently drops the Authorization header, causing intermittent 401s. Use when reviewing or debugging FastAPI/Starlette (or similar) routes, auth-token loss across redirects, or flaky authenticated endpoints.

SKILL.md

9.3 KB, as published. Nobody here has run it

qauthtrailingslash — Trailing-slash / 307-redirect audit

Audits every client URL in changed code against the FastAPI route definitions, reporting any mismatch that would 307-redirect at runtime. On cross-origin staging deployments, those 307s strip the Authorization header, so the request silently succeeds-as-401 and the UI shows empty data with no error.

Multi-repo contract: Runs across every repo in $SKILLS_ROOT/qship/repos.json that contains FastAPI route definitions. Single-repo users see one service's routes audited. Multi-service users see all services audited and any cross-service URL mismatch flagged.

This is an autonomous, machine-checkable audit — no human judgement required. Run it before merge on any PR that adds or moves an HTTP call.

Why this matters (the failure mode)

Memory references:

  • feedback_fastapi_trailing_slash_redirect.md — POST to a route without trailing slash, when the server route has one (or vice versa), gets a 307 redirect from FastAPI's default redirect_slashes=True.
  • feedback_cross_origin_redirect_strips_auth.md — Browsers strip the Authorization header on cross-origin redirects (CORS). Staging serves UI and API from different origins, so the redirect lands on the API as unauthenticated.

Symptom: silent empty-data UI on staging that works fine locally (same-origin in dev). Fingerprint: a 307 followed by a 401 in the network tab; your repo logs show "No credentials provided".

What the audit checks

For every .jsx / .tsx / .ts / .js / .py file in the diff (or current branch vs develop):

  1. Parse client URLs — extract every fetch / callApi / callApiAbs / _clientRequest / _doRequest / curl / httpx call's URL.
  2. Parse FastAPI routes — for every .py file under {{CODEBASE_PATH_PREFIX}}/*/api/v1/*.py, find each router declaration (APIRouter(prefix="/...")) and each handler decorator (@router.<method>("/...")). Resolve to the full URL.
  3. Cross-check — for each client URL, locate its matching server route by path-template + method. Report a mismatch when:
    • Server has trailing slash, client doesn't (POST/PATCH/PUT will 307).
    • Client has trailing slash, server doesn't.
    • Method/path otherwise match but slash position differs.

Argument parsing

Parse $ARGUMENTS to extract optional PR URL(s) or repo paths.

  • No args → audit current branch in the cwd against develop.
  • One or more PR URLs → check out each PR's branch, audit, restore.
  • A repo path → audit that repo's current branch against develop.

Steps

Step 1 — Resolve scope

If a PR URL is provided, derive <REPO_PATH> from the URL using the standard mapping:

RepoLocal path
{{PRIMARY_REPO_NAME}}{{CODEBASE_ROOT}}/{{PRIMARY_REPO_NAME}}
{{PRIMARY_REPO_NAME}}{{CODEBASE_ROOT}}/{{PRIMARY_REPO_NAME}}
{{PRIMARY_REPO_NAME}}{{CODEBASE_ROOT}}/{{PRIMARY_REPO_NAME}}

Fetch + checkout the PR branch in that repo path.

If no PR provided, use the current cwd. Fail loudly if cwd isn't one of the three repos.

Step 2 — Build the route table

Parse every .py file under <REPO_PATH>/{{CODEBASE_PATH_PREFIX}}/**/api/v1/*.py:

# pseudo
for py_file in glob("{{CODEBASE_PATH_PREFIX}}/**/api/v1/*.py", recursive=True):
    src = open(py_file).read()
    # 1. Find APIRouter declarations and their prefixes
    # 2. Find @<router>.<method>("/...") decorators
    # 3. Resolve full path = router.prefix + decorator path
    # 4. Record (method, full_path) -> py_file:line

Cross-repo audits: when the changed repo is {{PRIMARY_REPO_NAME}} but a UI call goes to {{PRIMARY_REPO_NAME}} (cross-repo URLs use apiBaseUrl / callApi / callApiAbs), parse {{PRIMARY_REPO_NAME}}'s routes too.

Output a route table:

GET    /organizations/{organization_id}/subjects/         {{PRIMARY_REPO_NAME}}/{{CODEBASE_PATH_PREFIX}}/{{PRIMARY_REPO_NAME}}/api/v1/subjects.py:62
POST   /organizations/{organization_id}/subjects/         {{PRIMARY_REPO_NAME}}/{{CODEBASE_PATH_PREFIX}}/{{PRIMARY_REPO_NAME}}/api/v1/subjects.py:208
GET    /organizations/{organization_id}/subjects/{subject_id}  {{PRIMARY_REPO_NAME}}/{{CODEBASE_PATH_PREFIX}}/{{PRIMARY_REPO_NAME}}/api/v1/subjects.py:142
...

Step 3 — Build the client URL list

For every changed .jsx/.tsx/.ts/.js file in the diff:

git -C <REPO_PATH> diff develop --name-only -- '*.jsx' '*.tsx' '*.ts' '*.js'

Parse out client URL calls. Patterns to extract:

# Direct fetch
fetch(`${base}/api/v1/.../...`)
fetch("/api/v1/...")

# Per-entity API helpers ({{PRIMARY_REPO_NAME}})
callApi("/...")            # path resolved against /api/v1/organizations/{orgId}
callApiAbs("${baseUrl}/...")  # absolute URL — extract path

# Policy API ({{PRIMARY_REPO_NAME}}-side)
_clientRequest("POST", "/...")   # path resolved against /api/v1/policies
_doRequest("GET", `/${id}`)          # same

# Curl / httpx (Python or shell)
curl ... "/api/v1/..."
httpx.get("/api/v1/...")

For each match, record (method, resolved_path, file:line).

Resolution rules:

  • callApi(path) resolves to /api/v1/organizations/{orgId}{path} against {{PRIMARY_REPO_NAME}}.
  • callApiAbs(absolute_url) — extract the /api/v1/... substring.
  • _clientRequest(method, path) resolves to /api/v1/policies{path} against {{PRIMARY_REPO_NAME}}.
  • _doRequest(method, path) (inside recordApi.js) — same as _clientRequest.

Step 4 — Cross-check + report

For each client URL (method, path):

  1. Look for an exact server-route match. If found → ✓.
  2. If not found, look for a match with the trailing slash flipped:
    • Client has trailing slash, server doesn't → ⚠️ FLIP-LIKELY-307.
    • Server has trailing slash, client doesn't → ⚠️ FLIP-LIKELY-307.
  3. If still not found → ⚠️ NO-MATCHING-ROUTE (might be a typo, removed endpoint, or cross-repo URL whose target wasn't parsed).

Output as a table:

TRAILING-SLASH AUDIT — <repo> @ <branch>
========================================

Client URLs scanned: 23
Server routes parsed: 412
Matched cleanly:     23 ✓
Slash mismatches:    0
Unmatched paths:     0

When mismatches exist:

⚠ MISMATCHES (would 307-redirect on cross-origin staging):

  1. POST /api/v1/policies/training
     Client: shared/recordApi.js:473  → "/training"  (no trailing slash)
     Server: api/v1/records.py:721   → "/training/" (trailing slash)
     Fix:    change client to "/training/" or server to "/training"

  ...

Step 5 — Severity classification

Not every mismatch is critical. Classify:

MethodTrailing slash impact
POST / PUT / PATCH / DELETEHIGH — body + auth header stripped on the 307 redirect in cross-origin context. Silent fail.
GET (read-only, no body)MEDIUM — request still completes via the 307→GET, but adds a round trip. Authorization header IS preserved on cross-origin GET redirects in modern browsers, BUT not on Safari + some proxies. Treat as still worth fixing.

Mismatches MUST be fixed (HIGH) or surfaced as a known minor (MEDIUM), never silently accepted.

Step 6 — Anti-patterns to flag separately

While scanning, also call out:

  • Hard-coded absolute URLs to a sibling service that ignore the configured base URL (e.g. fetch("http://{{PRIMARY_REPO_NAME}}...") or relative /api/v1/policies/... calls when {{PRIMARY_REPO_NAME}} now ships the per-tenant appApiUrl config knob). These should go through the configured-base-URL helper.
  • fetch(...) directly inside a component instead of through the per-entity API module. Cite the CLAUDE.md anti-pattern bullet.

Step 7 — Exit code + summary

  • All client URLs match → exit 0 with summary.
  • Any HIGH mismatch → exit 1 with the table; the user must fix before merge.
  • Only MEDIUM mismatches → exit 0 but print the table prominently.

Implementation note

The implementation may be a Python script rather than pure shell — easier to parse APIRouter prefixes + decorator path strings reliably. A reasonable starting shape lives at ~/.claude/scripts/qauthtrailingslash.py; the command may invoke it via python ~/.claude/scripts/qauthtrailingslash.py [args].

If a clean implementation isn't obvious, fall back to a guided audit: enumerate the changed client URLs and the matching server-route paths inline in the chat, then ask the user to confirm each match or flip.

Output rules

  • Always print the route table count and the client-URL count up front.
  • Always show the actual file:line citations for any mismatch — reviewers must be able to click into the code.
  • Never auto-fix. The fix could be at either side (client or server) and that's a domain decision.

Related commands

  • /qfixci — runs after CI failures; trailing-slash bugs typically don't fail CI but DO fail on staging post-deploy.
  • /qmanualt — E2E tests on staging will catch these as silent empty-UI symptoms; this command catches them earlier.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.