# audit/bench report —/claude-code-templates
Link-> https://audit-bench-app-242355763105.europe-west1.run.app
- Verdict: Needs work
- Framework: Python
- Files scanned: 15/3000
- Scanned: 12/07/2026, 15:48:12
Reviewed 15/15 files (of 3000 total) — 0 from cache, 5 needed no AI review. Found 44 finding(s), 0 vulnerable dependency issue(s), 0 circular import chain(s), 30 possibly dead file(s), 10 duplicate block(s), 8 potential secret(s).
api/_lib/auth.js
No issues in this file.
api/_lib/neon.js
No issues in this file.
api/tests/endpoints.test.js
No issues in this file.
api/claude-code-monitor/discord-notifier.js
HIGH · Security — Unescaped user content can trigger mentions/formatting in Discord (line 81)
Values from changes (e.g. changes.breaking, changes.features) and the version string are used directly in embed fields. Malicious or accidental content containing @everyone, @here, user mentions, or Discord markdown can cause unwanted mentions or formatting in the channel where the bot posts.
Suggested fix: Sanitize/escape embed text to neutralize Discord mention tokens (@everyone, @here, <@...>) and escape markdown where appropriate. Consider stripping or encoding mention-like patterns, and/or explicitly disable pinging where the API supports it. Also normalize the change lists into safe, plain-text bullet lists.
function escapeDiscord(text){ return text.replace(/@everyone|@here|<@!?\d+>/g,'`$&`').replace(/([*_`~|>])/g,'\\$1'); }
// then use: value: escapeDiscord(joinedChanges)MEDIUM · Logic — Array values are injected directly, causing poor formatting (line 81)
The code checks for changes.breaking.length and then sets value: changes.breaking. If changes.breaking is an array, embedding it directly will call toString() producing comma-separated values without bullets or newlines, which is less readable and may be unintended.
Suggested fix: If change categories are arrays, join them into newline-prefixed bullets (e.g. changes.breaking.map(x => - ${escape(x)}).join('\n')) and escape content.
if (changes.breaking && changes.breaking.length>0){ const list = changes.breaking.map(x => `- ${escapeDiscord(String(x))}`).join('\n'); embed.fields.push({ name:'⚠️ Breaking Changes', value: list, inline:false }); }MEDIUM · Maintainability — No validation or truncation against Discord field length limits (line 116)
Discord limits embed field values to 1024 characters (and overall embed size limits). The function does not validate or truncate long values (e.g. very long changelogs or URLs), which can cause API errors when sending the embed.
Suggested fix: Validate and truncate field values to safe lengths (e.g. 1000 chars), appending an ellipsis and a link to the full changelog. Centralize truncation logic to ensure consistent behavior.
function truncate(str,limit=1000){ if(str.length<=limit) return str; return str.slice(0,limit-1)+'…'; }
// use: value: truncate(formattedValue, 1000)LOW · Security — Unvalidated URLs used in embed.url and links (line 69)
npmUrl and githubUrl are used directly in embed.url and as markdown links. If these values are attacker-controlled, they might point to malicious or unexpected locations. While Discord likely enforces safe linking, it's better to validate URLs to ensure they use https and expected domains.
Suggested fix: Validate that npmUrl/githubUrl are valid https URLs and optionally restrict to known hosts (npmjs.com, github.com). Fallback to plain text if invalid.
function validHttpsUrl(u, allowedHosts){ try{ const url=new URL(u); return (url.protocol==='https:') && (!allowedHosts || allowedHosts.includes(url.hostname)); }catch(e){return false;} }
// usage: embed.url = validHttpsUrl(githubUrl, ['github.com']) ? githubUrl : undefined;api/_parser-claude.js
MEDIUM · Logic — Unreachable / shadowed 'deprecate' category (line 107)
The code checks for 'deprecated' and 'removed' early and returns 'breaking', then later has a separate check for 'deprecate' returning 'deprecation'. Any description containing 'deprecate' (or 'deprecated') will be caught by the earlier 'deprecated' check and classified as 'breaking', making the later 'deprecation' branch unreachable for typical inputs.
Suggested fix: Decide the intended precedence and reorder checks accordingly. If 'deprecation' should be a distinct class, check for 'deprecate'/'deprecated' before the 'breaking' group or refine the 'breaking' group to exclude deprecations.
function classifyChange(description) {
const lower = description.toLowerCase();
// Deprecations (check before breaking if you want to treat deprecations separately)
if (lower.includes('deprecate') || lower.includes('deprecated')) {
return 'deprecation';
}
// Breaking changes
if (lower.includes('breaking') || lower.includes('removed')) {
return 'breaking';
}
...
}
LOW · Maintainability — Fragile substring matching leads to false positives/negatives (line 111)
The function uses naive substring checks (e.g. lower.includes('add'), lower.includes('fix'), lower.includes('support for')). This can match unintended words (e.g. 'address' contains 'add') or miss inflections and punctuation, and emoji checks use startsWith which may fail with leading whitespace or different Unicode normalization.
Suggested fix: Use regular expressions with word boundaries (\b) to avoid partial-word matches, normalize Unicode/trim input for emoji checks, and centralize keyword lists to reduce duplication. Consider stemming/lemmatization or a small NLP approach if higher accuracy is needed.
function classifyChange(description) {
const text = (description || '').trim();
const lower = text.toLowerCase();
const word = (kw) => new RegExp("\\b" + kw + "\\b", 'i');
if (word('deprecate').test(lower) || word('deprecated').test(lower)) return 'deprecation';
if (word('breaking').test(lower) || word('removed').test(lower)) return 'breaking';
if (word('add').test(lower) || word('new').test(lower) || /\bintroduce\b/.test(lower) || /support for/.test(lower) || /^✨|^/.test(text)) return 'feature';
// ...and so on
}
LOW · Maintainability — High cyclomatic complexity from repeated literal checks (line 105)
The function contains many separate conditional branches implemented as repeated string checks, which increases cognitive load and is error-prone when adding/removing keywords.
Suggested fix: Refactor to use a table-driven approach: maintain a prioritized array of {category, patterns} and iterate over it. This reduces duplication and makes the function easier to extend and test.
const CATEGORIES = [
{ name: 'deprecation', patterns: [/\bdeprecate(d)?\b/i] },
{ name: 'breaking', patterns: [/\bbreaking\b/i, /\bremoved\b/i] },
{ name: 'feature', patterns: [/\badd\b/i, /\bnew\b/i, /^✨|^/] },
// ...
];
function classifyChange(description) {
const text = (description || '').trim();
for (const c of CATEGORIES) {
if (c.patterns.some(p => p.test(text))) return c.name;
}
return 'other';
}
api/claude-code-monitor/webhook.js
No issues in this file.
api/claude-code-monitor/parser.js
HIGH · Logic — Naive substring matching causes false positives and incorrect classifications (line 105)
The function uses simple .includes() and .startsWith() checks on a lowercased string (variable lower). This leads to false positives: e.g. 'address' contains 'add' and would be classified as 'feature', 'prefix' contains 'fix' and could be classified as 'fix', or 'performance' inside another word could trigger 'performance' unexpectedly. Also single-word checks like 'new' will match many words (e.g. 'renew'). These misclassifications will produce incorrect change labels.
Suggested fix: Use word-boundary aware regular expressions (e.g. \badd\b) or tokenize the description into words and match against a set of canonical keywords. Normalize Unicode and case first, and consider stemming or lemmatization if you need to match multiple word forms. Test with representative examples to ensure few false positives.
function classifyChange(description) {
const text = (description || '').normalize('NFKC').toLowerCase();
const categories = {
breaking: [/\bbreaking\b/, /\bremoved\b/, /\bdeprecated\b/],
feature: [/\badd\b/, /\badded\b/, /\bnew\b/, /\bintroduce\b/, /\bsupport for\b/, /^✨/, /^/],
fix: [/\bfix\b/, /\bfixes\b/, /\bresolve\b/, /\bresolved\b/, /\bcorrect\b/, /\bpatch\b/, /^/],
improvement: [/\bimprove\b/, /\benhance\b/, /\boptimi[sz]e\b/, /\bbetter\b/, /\brefactor\b/, /^⚡/, /^♻️/],
deprecation: [/\bdeprecate\b/, /\bdeprecated\b/],
performance: [/\bperformance\b/, /\bspeed\b/, /\bfaster\b/],
documentation: [/\bdocs?\b/, /\bdocumentation\b/]
};
for (const [label, patterns] of Object.entries(categories)) {
for (const rx of patterns) {
if (rx.test(text)) return label;
}
}
return 'other';
}
MEDIUM · Maintainability — High cyclomatic complexity and duplicated/ordered checks make behavior brittle (line 105)
The function enumerates conditions inline, repeating similar checks across branches and relying on the order to resolve conflicts (e.g. 'deprecated' appears both in the 'breaking' branch and in the 'deprecation' branch). This increases the chance of subtle bugs when adding or changing keywords and makes the function hard to extend (e.g. adding new synonyms or emoji rules).
Suggested fix: Refactor to a data-driven approach: store lists/regexes per category and iterate in a clear priority order. Consolidate duplicates (decide category precedence explicitly). Add unit tests covering ambiguous cases and examples with punctuation, plurals, and compound words. Keep emoji checks separate if they must match prefixes rather than words.
See the examplePatch in the previous finding: categories defined as a map of regex arrays and a single loop reduces branching and duplication, and makes adding/removing keywords straightforward.LOW · Logic — Emoji checks combined with toLowerCase() are unusual but not harmful (line 116)
The function lowercases the whole description into variable lower and then calls startsWith on emoji strings (e.g. lower.startsWith('✨')). Lowercasing has no effect on emoji characters, but it is inconsistent stylistically and could hide issues if other Unicode normalization matters (e.g. combining characters or alternative emoji presentations).
Suggested fix: Normalize the input string (e.g. description.normalize('NFKC')) before lowercasing. If emoji prefixes are important, check them on the original normalized string as well or convert both to a normalized form.
const normalized = (description || '').normalize('NFKC');
const lower = normalized.toLowerCase();
if (normalized.startsWith('✨') || normalized.startsWith('')) { ... }api/claude-code-monitor/check-version.js
HIGH · Security — Discord webhook URL (secret) is written to DB/logs (line 206)
The code inserts the webhook URL stored in environment variables into the discord_notifications_log table (line ~206 in the handler snippet). Discord webhook URLs are secrets; persisting them in plaintext in the database increases blast radius if the DB is compromised and may violate least-privilege practices.
Suggested fix: Do not store full webhook URL. Store a hash or fingerprint (e.g., HMAC or SHA256) of the webhook URL if you need to correlate logs to a particular webhook, or store an ID/reference. Ensure logs and DB do not contain secrets.
const webhook = process.env.DISCORD_WEBHOOK_URL_CHANGELOG || process.env.DISCORD_WEBHOOK_URL;
const webhookFingerprint = webhook ? crypto.createHash('sha256').update(webhook).digest('hex') : null;
... VALUES ( ..., ${webhookFingerprint}, ... )HIGH · Security — Sensitive data may be leaked via error storage (line 283)
In the catch block the code updates monitoring_metadata.last_error with error.message (line ~283). Error messages may contain sensitive internal data, stack snippets, or secrets (e.g., if an upstream response includes tokens). Persisting raw error messages in DB increases risk of exposing sensitive info.
Suggested fix: Sanitize and truncate error messages before persisting. Consider storing only an error code, an abbreviated message, and keep full details in secure logs accessible to operators. Respect PII/secret handling policies.
const safeMsg = error && error.message ? String(error.message).slice(0, 1000) : 'Unknown error';
... last_error = ${safeMsg} ...MEDIUM · Security — Webhook URL used directly for outbound request (SSRF-like risk if env compromised) (line 86)
sendToDiscord posts to webhookUrl read from environment (line ~86). If the environment value is compromised or attacker-controlled, the service could be used to make requests to arbitrary endpoints. There is no validation to ensure the URL is a Discord webhook (hostname/path pattern) or that it uses HTTPS.
Suggested fix: Validate webhookUrl before use: ensure scheme is https, hostname ends with discordapp.com/discord.com, and path pattern matches Discord webhook structure. Alternatively, restrict allowed hostnames via configuration or use a proxy service that enforces destination constraints.
const parsedUrl = new URL(webhookUrl);
if (parsedUrl.protocol !== 'https:' || !/discord(?:app)?\.com$/.test(parsedUrl.hostname)) {
throw new Error('Invalid Discord webhook URL');
}MEDIUM · Logic — Discord embed fields not validated/truncated against Discord limits (line 22)
The embed is assembled directly from formatted content and may exceed Discord limits (per-field 1024 chars, embed total <= 6000 chars). If too large, Discord will reject the webhook (axios.post will fail) and the function does not handle partial truncation or fallback, leading to failed notifications and possible error handling paths.
Suggested fix: Validate and truncate field values to meet Discord limits. For long changelogs, attach a short summary in the embed and include a link to the full changelog instead of full text. Catch and handle HTTP errors from Discord and log response bodies for diagnostics (without leaking secrets).
function truncateForDiscord(s) { return s.length > 1000 ? s.slice(0, 997) + '...' : s; }
embed.fields.push({ name: '✨ New Features', value: truncateForDiscord(formatted.features), inline: false });MEDIUM · Maintainability — No retry/backoff around external network calls (line 127)
The handler makes multiple network calls (axios.get(CHANGELOG_URL), getLatestNPMVersion, axios.post to Discord) without retries or circuit-breaker behavior. Transient network failures will cause entire processing to fail and increment error counters; intermittent third-party outages could reduce reliability.
Suggested fix: Introduce retry/backoff for idempotent GETs and POSTs (with bounded retries), or use an HTTP client wrapper (axios-retry, or implement exponential backoff). Consider persistent job queue for retries of notifications.
const axiosInstance = axios.create();
axiosRetry(axiosInstance, { retries: 3, retryDelay: axiosRetry.exponentialDelay });
const changelogResponse = await axiosInstance.get(CHANGELOG_URL);
const response = await axiosInstance.post(webhookUrl, payload);LOW · Architecture — Permissive CORS (Access-Control-Allow-Origin: *) (line 104)
Handler sets Access-Control-Allow-Origin to '*' (line 104). If this endpoint is intended only for internal/cron use, exposing it cross-origin may allow other web pages to interact with it in the browser context. This broad allowance can increase risk of CSRF-like interactions if credentials/cookies were in use (currently none shown), and it's generally better to restrict origins.
Suggested fix: Restrict allowed origins to known frontends or remove CORS entirely if endpoint is not consumed from browsers. Use environment config to allow '*' only in development.
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'https://your.admin.console';
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);LOW · Maintainability — Potentially excessive DB writes for each change without batching (line 165)
The code inserts each changelog item in a loop with an individual INSERT (lines ~165-174). For many changes this causes many DB roundtrips which impacts latency and cost.
Suggested fix: Use a bulk insert or transaction to insert all changes in a single query, or at least use a transaction and parameterized multi-row insert to reduce roundtrips.
await sql`INSERT INTO claude_code_changes (version_id, change_type, description, category) VALUES ${sql(parsed.changes.map(c => [versionId, c.type, c.description, c.category]))}`;api/claude-code-check.js
HIGH · Security — Unauthenticated public endpoint can be abused to trigger Discord webhooks (line 103)
handler exposes a POST/GET endpoint with Access-Control-Allow-Origin: '*' and no authentication, authorization, or rate limiting. Any client (including automated scripts) can call this endpoint to perform the full flow, including sending messages to the configured Discord webhook. An attacker can spam the webhook or cause excessive database writes and external requests.
Suggested fix: Require authentication or an allowlist for callers (API key, HMAC signature, or restrict to internal network). Remove or restrict CORS to trusted origins. Add rate limiting and idempotency checks (beyond DB checks) to prevent repeated triggers from untrusted callers.
Add a check for a pre-shared header/API key and restrict CORS:
// at top of handler
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'https://your-admin.example.com';
res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGIN);
// authenticate
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.CLAUDE_MONITOR_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Consider adding simple rate limiting with an in-memory or Redis counter
HIGH · Security — Discord webhook URL (secret) is persisted and potentially exposed (line 235)
Inserting the webhook_url into discord_notifications_log (line ~235) stores the raw webhook URL in the database. Webhook URLs are secrets that allow anyone holding them to post to the Discord channel; storing them in plaintext increases risk of leakage (DB backups, logs, or other DB consumers). Also process.env is used directly in the insert which may result in storing multiple variants.
Suggested fix: Avoid storing full webhook URLs. Store only a non-sensitive identifier (e.g., a webhook id or hash). If you must store it, encrypt it at rest or store a keyed hash (HMAC) rather than plaintext. Ensure logging does not print the full secret.
const webhookUrl = process.env.DISCORD_WEBHOOK_URL_CHANGELOG || process.env.DISCORD_WEBHOOK_URL;
const webhookHash = crypto.createHmac('sha256', process.env.LOG_HMAC_KEY).update(webhookUrl).digest('hex');
// store webhook_hash instead of webhook_url
INSERT INTO discord_notifications_log (version_id, webhook_hash, payload, response_status, response_body) VALUES (...)MEDIUM · Security — CORS wildcard combined with state-changing endpoint enables CSRF from browsers (line 105)
res.setHeader('Access-Control-Allow-Origin', '*') allows any web page to make requests from a browser to this endpoint. Even though typical CSRF protections rely on cookies/credentials, if any clients rely on browser-side credentials or other implicit auth, this is risky. At minimum this increases the risk that benign pages or third-party scripts can cause your server to send Discord notifications.
Suggested fix: Limit Access-Control-Allow-Origin to known admin/orchestrator origins or remove it entirely for a private endpoi
Source: davila7/claude-code-templates