diff --git a/commands/paymob-check-hmac.md b/commands/paymob-check-hmac.md index 4d8d732..6401398 100644 --- a/commands/paymob-check-hmac.md +++ b/commands/paymob-check-hmac.md @@ -2,6 +2,7 @@ description: Audit a Paymob webhook HMAC verification implementation for correctness and safe failure behavior. argument-hint: "[path to the webhook handler, optional]" disable-model-invocation: true +allowed-tools: Read Glob Grep --- Audit the user's Paymob webhook HMAC verification code: $ARGUMENTS @@ -10,14 +11,25 @@ Audit the user's Paymob webhook HMAC verification code: $ARGUMENTS - Never ask the user to paste their HMAC secret, API key, or Secret Key into this conversation. - Never read a `.env` file's actual secret value, and never echo, print, or write any secret into chat output, logs, or a file you create. - The default mode is a **static code audit** — no computation is required to complete it. +- **Never audit the field order, digest, or callback shape from memory.** This command deliberately carries no copy of the spec. If you cannot read `hmac-verification.md`, you cannot do this audit — say so and stop. An audit performed from recall can pass a wrong field order as correct, which is the exact failure this command exists to catch: HMAC verification that looks verified and silently is not. + +## Step 0 — locate the spec + +`hmac-verification.md` lives in the skill's `references/` directory. Use the first of these that exists: + +1. `${CLAUDE_PLUGIN_ROOT}/skills/paymob-integration/references/` — plugin install +2. `~/.claude/skills/paymob-integration/references/` — personal skill install. `~` is the user's home directory; on Windows expand it yourself (`%USERPROFILE%\.claude\skills\…`), since a literal `~` will not resolve there. +3. `skills/paymob-integration/references/` — repository checkout, relative to the working directory. This only resolves if the user happens to be working inside the skill repo, so treat path 2 as the reliable one for a personal install. + +`${CLAUDE_PLUGIN_ROOT}` is substituted only for plugin installs. If you see it unexpanded, you are not in one — use path 2 or 3. If none resolve, search for `hmac-verification.md` under any `paymob-integration` directory. Confirm you have actually read the file before starting Step 2. ## Step 1 — locate the code If `$ARGUMENTS` names a path, start there. Otherwise search for the callback route bound to `notification_url` / the `hmac` query parameter, or for `PAYMOB_HMAC_SECRET` usage. -## Step 2 — audit against `${CLAUDE_PLUGIN_ROOT}/skills/paymob-integration/references/hmac-verification.md` +## Step 2 — audit against `references/hmac-verification.md` -Read that file first — don't rely on memory for the field order or algorithm, since this command carries no copy of the spec. Then check each item below and report pass/fail with file/line references: +Read that file first, from the root you resolved in Step 0 — don't rely on memory for the field order or algorithm. Then check each item below and report pass/fail with file/line references: 1. **Digest**: HMAC-**SHA-512**, not SHA-256 or any other digest. 2. **Source and order**: the concatenation reads fields from `body.obj` (the POST payload) in exactly the field order given in `hmac-verification.md` — flag any other ordering, including "alphabetical by key name," as a failure. diff --git a/commands/paymob-explain-error.md b/commands/paymob-explain-error.md index 6c9c1ba..0b51899 100644 --- a/commands/paymob-explain-error.md +++ b/commands/paymob-explain-error.md @@ -1,12 +1,22 @@ --- description: Explain a Paymob error code, HTTP status, or error message and give the cause and fix. argument-hint: "[error code, HTTP status, or error message]" -allowed-tools: Read +allowed-tools: Read Glob --- The user wants this Paymob error explained: $ARGUMENTS -1. Read the **Troubleshooting** table in `${CLAUDE_PLUGIN_ROOT}/skills/paymob-integration/SKILL.md` and match `$ARGUMENTS` against its rows (by HTTP status, error text, or symptom). Quote the cause(s) listed for the matching row — do not paraphrase from memory, read the table each time in case it has changed. +**Locating the skill files.** `SKILL.md` and its `references/` directory live together. Use the first of these that exists: + +1. `${CLAUDE_PLUGIN_ROOT}/skills/paymob-integration/` — plugin install +2. `~/.claude/skills/paymob-integration/` — personal skill install. `~` is the user's home directory; on Windows expand it yourself (`%USERPROFILE%\.claude\skills\…`), since a literal `~` will not resolve there. +3. `skills/paymob-integration/` — repository checkout, relative to the working directory + +`${CLAUDE_PLUGIN_ROOT}` is substituted only for plugin installs. If you see it unexpanded, you are not in one — use path 2 or 3. If none resolve, search for `SKILL.md` under any `paymob-integration` directory. Paths written as `references/…` below are relative to whichever root you resolved. + +**If you cannot read the files, say so and stop.** Do not explain a Paymob error from memory — a plausible-sounding wrong cause sends the merchant to debug the wrong layer. + +1. Read the **Troubleshooting** table in `SKILL.md` and match `$ARGUMENTS` against its rows (by HTTP status, error text, or symptom). Quote the cause(s) listed for the matching row — do not paraphrase from memory, read the table each time in case it has changed. 2. If the fix involves code, open the reference file matching the user's project language/framework — `references/code-nodejs.md`, `references/code-python.md`, `references/code-php.md`, `references/code-dotnet.md`, `references/code-ruby.md`, or `references/code-frontend.md` — and adapt its corrected snippet to the user's project. Do not write a fix that contradicts what's in that file (for example, don't invent a different HMAC digest, header scheme, or amount unit than what the reference shows). 3. If the matching row is the HMAC-mismatch row, also read `references/hmac-verification.md` before answering and get the field order, digest algorithm, and the `obj.id`/`order.id` (POST) vs `id`/`order_id` (GET redirect) distinction from that file directly — never restate the field order from memory or from this command file, since this file intentionally carries no copy of it. 4. If `$ARGUMENTS` does not match any row in the table, say so explicitly rather than guessing at a cause. Then: diff --git a/commands/paymob-test-cards.md b/commands/paymob-test-cards.md index 6ccd132..ee17dae 100644 --- a/commands/paymob-test-cards.md +++ b/commands/paymob-test-cards.md @@ -1,10 +1,20 @@ --- description: Show Paymob sandbox test card and wallet credentials, optionally filtered by payment method. argument-hint: "[card|wallet|kiosk|bnpl]" -allowed-tools: Read +allowed-tools: Read Glob --- -Read `${CLAUDE_PLUGIN_ROOT}/skills/paymob-integration/references/test-credentials.md` in full and present the sandbox test credentials it contains to the user. Do not use any test numbers from memory — this file is the only source. +Read `references/test-credentials.md` in full and present the sandbox test credentials it contains to the user. Do not use any test numbers from memory — this file is the only source. + +**Locating the reference file.** Use the first of these that exists: + +1. `${CLAUDE_PLUGIN_ROOT}/skills/paymob-integration/references/` — plugin install +2. `~/.claude/skills/paymob-integration/references/` — personal skill install. `~` is the user's home directory; on Windows expand it yourself (`%USERPROFILE%\.claude\skills\…`), since a literal `~` will not resolve there. +3. `skills/paymob-integration/references/` — repository checkout, relative to the working directory + +`${CLAUDE_PLUGIN_ROOT}` is substituted only for plugin installs. If you see it unexpanded, you are not in one — use path 2 or 3. If none resolve, search for `test-credentials.md` under any `paymob-integration` directory. + +**If you cannot read the file, say so and stop.** Do not list card numbers from memory — stale or invented test credentials waste a merchant's debugging time on a payment that was never going to succeed. Argument handling for `$ARGUMENTS`: - `card` → show only the Mastercard and Visa test card sections. diff --git a/scripts/validate.py b/scripts/validate.py index 46cf7db..82212eb 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -64,6 +64,22 @@ HMAC_FIELD_ORDER_RESTATEMENT_PATTERN = re.compile( r"amount_cents\s*\r?\n\s*created_at\s*\r?\n\s*currency" ) +# ${CLAUDE_PLUGIN_ROOT} is substituted only for plugin installs. A command that +# names it as the sole location breaks when the skill is installed as a personal +# skill under ~/.claude/skills/, which is what agents do when asked to install +# the skill from GitHub. Every command must offer a non-plugin fallback. +COMMAND_FALLBACK_PATH_PATTERNS = ( + re.compile(r"~/\.claude/skills/paymob-integration"), + re.compile(r"(? dict: @@ -488,6 +504,20 @@ def validate_commands(errors: list[str]) -> None: if COMMAND_SECRET_PATTERN.search(body): errors.append(f"{label} appears to contain a hardcoded secret or key") + if "${CLAUDE_PLUGIN_ROOT}" in body and not any( + pattern.search(body) for pattern in COMMAND_FALLBACK_PATH_PATTERNS + ): + errors.append( + f"{label} names ${{CLAUDE_PLUGIN_ROOT}} without a non-plugin fallback " + "path; the reference files are unreachable in a personal-skill install" + ) + + if not COMMAND_FAIL_CLOSED_PATTERN.search(body): + errors.append( + f"{label} must tell the agent to stop rather than answer from memory " + "when it cannot read its reference file" + ) + def validate_links(errors: list[str]) -> None: for markdown in ROOT.rglob("*.md"):