finance-pulse — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited finance-pulse (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
/finance/finance-cleanupGive the user a 30-second financial check-in. One number, a few flags, prospective framing. Read-only — this skill never writes.
~/.claude/copilot-money/user-profile.md. If the file doesn't exist: run mkdir -p ~/.claude/copilot-money, then copy skills/user-profile.template.md (relative to the copilot-money-mcp repo root) to the profile path. First-time bootstrap may require CWD = repo root for the template read. Note:get_transactions_live (live), you are in live mode — always the case when write tools are on. If it has get_transactions (cache) instead, you are in cache mode. Each step below names the tool for each mode; call only the one for your mode. Run these in parallel:get_accounts_live (live) / get_accounts (cache).get_transactions_live (live) / get_transactions (cache), each with exclude_transfers: true and one of period: "this_month", period: "last_month", period: "last_90_days". Both tools take these arguments.fields: ["default", "type", "recurring_id"] to get_transactions_live (live): type is how 2.2 keeps REGULAR rows only, and recurring_id is what the 2.5 price-drift check groups by. Neither is in the default row.fields argument is needed on get_transactions (cache): the default row has what this skill computes from it (amounts, category names, merchant names, dates).get_categories_live (live) / get_categories (cache). Per-category spending is computed from the transaction rows (2.2, 2.4), not read from this tool, so the figure is the same in both modes.get_recurring_live (live), default rows. These carry state, nextPaymentDate and nextPaymentAmount but no charge history: their payments holds only the next payment.get_recurring_transactions (cache) with period: "last_90_days" and fields: ["default", "transactions"]. The fields argument is what the cache-mode price-drift check in 2.5 reads: the matched per-charge transactions array is not in the default row.get_upcoming_recurrings_live (live). Needed for the "spent this month" figure in 2.2; see the caveat there about its horizon.get_budgets_live (live) with months_window: 1. Only the current month is needed, and the default returns 12 months per budget.get_budgets (cache) with active_only: true.get_goals with active_only: true (both modes).Transaction reads return 100 rows per page; page through with limit and offset until a page comes back short.
Run this BETWEEN Phase 1's read and Phase 2's compute. It catches the common case where the profile was bootstrapped months ago and the underlying numbers (income, fixed obligations, account roles) have drifted.
## Income & Obligations## Irregular Expenses (Sinking Funds)## Accounts## Savings & Goals is intentionally NOT in this list — the savings target comes live from get_goals each session, so there's no stale-profile concern. The template doesn't carry a last_verified stamp for that section.
Each section has a comment of the form <!-- last_verified: YYYY-MM-DD --> or <!-- last_verified: never -->. Extract the date.
<!-- staleness_threshold_days: <N> ... --> — extract the first integer after the colon (any trailing text is human-readable documentation only). Default to 90 if the comment is absent or the value can't be parsed.last_verified: never → "never verified"Example prompt: "Your profile sections are stale: [Income & Obligations] last verified 142 days ago; [Accounts] never verified. Re-bootstrap? [y/skip]". A single y/skip applies to all stale sections.
<!-- last_verified: --> comment to today's date in YYYY-MM-DD form.last_verified dates to Phase 3 for warning output.If ~/.claude/copilot-money/user-profile.md has empty Income & Obligations section, bootstrap it:
get_accounts_live (live) / get_accounts (cache):start_date and end_date (e.g., start_date: "2025-03-13", end_date: "2026-04-13" — there is no "last_13_months" period shorthand). This is NOT the 90-day window used for trends — annual charges need a full year to detect. Scan for:~/.claude/copilot-money/user-profile.md. For each section saved, also update its <!-- last_verified: --> HTML comment to today's date in YYYY-MM-DD form (this is what Phase 1.5 reads to decide if the section is stale). On subsequent runs, skip bootstrapping and read the profile directly.Whenever you report what the user has "spent this month" (Quick Stats, a category total, "how much have I spent"), compute it the way the Copilot app does, so the number matches the one on their screen. Summing every amount does not: income rows are negative and net against all spending.
exclude_transfers and exclude_excluded default to true. One gap in live mode: Copilot's API exposes no per-transaction exclusion flag, so a row the user excluded individually, in a category that is not excluded, cannot be filtered out. The live total can therefore run slightly high. Cache mode filters both kinds of exclusion.type is REGULAR, which drops INCOME. Cache mode: rows carry no type, so rely on step 3, which drops income because Copilot leaves income uncategorized (Rule 7).category_name). The app does not count them.amount within each category, so a refund (negative) offsets a purchase in the same category. The sum over categories is the posted spend.get_upcoming_recurrings_live (live). It appears to return only bills due within about the next 14 days, so later in the month, add every get_recurring_live (live) row whose state is active and whose nextPaymentDate falls in this month, and drop duplicates by id. Use nextPaymentAmount for each.next_expected_date and average_amount from get_recurring_transactions (cache) when the date falls in the rest of this month. This is an approximation: the cache has no "paid" flag. Say so if the figure matters.Spent this month = posted spend (step 4) + unpaid recurrings (step 5). Do the arithmetic in Python.
Use Python via Bash for all arithmetic. The formula:
Free Money = Net Monthly Income
− Fixed Obligations (rent, utilities, insurance, loans)
− Savings Target (from goals or profile)
− Amortized Irregular Expenses (sinking fund reserve)
− Already Spent This Month (non-fixed, non-savings spending)Steps:
get_goals or profile. If get_goals returns 0 AND the profile has no savings target, set this term to 0 and note it for Phase 3 output: "No savings target set — Free Money doesn't reserve anything for savings."Also compute:
Use Python to compare this month's spending by category against 90-day rolling monthly averages. Compute each window's per-category totals from its transaction rows with 2.2 steps 1–4 (net per category, REGULAR rows, uncategorized skipped):
From the recurring rows pulled in Phase 1:
nextPaymentAmount in live mode, average_amount in cache mode.nextPaymentDate (live) or next_expected_date (cache) 7 or more days in the past.get_transactions_live (live) rows, which Phase 1 pulled with fields: ["default", "type", "recurring_id"]. Group them by recurring_id, match each group to its recurring by id, and compare the amounts in date order.transactions array from get_recurring_transactions (cache), which Phase 1 requested with fields: ["default", "transactions"].get_recurring_transactions (cache) also detects recurring patterns Copilot is not tracking. Flag any that aren't in the Copilot subscriptions list. Live mode returns only the recurrings Copilot tracks, so skip this flag there.Scan this month's transactions for:
Prioritize using 3-tier system:
Tone: Match ~/.claude/copilot-money/user-profile.md Communication Style. Default: blunt, simple, dollar amounts.
Stale-data warning prefix (if Phase 1.5 flagged any stale sections that the user chose to skip). Prepend the output with a warning block, then continue with The Number / Flags / Quick Stats as normal:
⚠️ Using stale profile data: - Income & Obligations: last verified [date] ([N] days ago) - Accounts: never verified
>
Re-run with re-bootstrap if these numbers feel off.Format each line based on the stamp value: a real date shows "[date] ([N] days ago)"; never shows "never verified".
Only include sections that were actually skipped — don't list every section unconditionally.
Structure — always this order:
Open with the Free Money figure, framed prospectively:
You have $X left to spend this month. That's $Y/day for the next Z days.
If runway is notably short or long, add context:
If this is the first run (bootstrap happened), frame it as: "Based on what I can see, here's where you stand:" and note that the numbers will get more accurate over time.
Present Tier 1 flags first, then Tier 2 if room. Each flag is one sentence:
Framing rules:
If the user wants more detail, append:
Cap total output. The entire pulse should fit in one screen — roughly 10-15 lines for "simple" detail level, up to 25 for "detailed". If you have more than 5 flags, pick the highest-priority ones.
After presenting, silently check if any profile sections should be updated:
Do not ask the user about profile updates during pulse. Pulse is a quick check-in. If profile needs significant updates, suggest: "Your profile might be out of date — want to run a quick update?"
set_*, no create_*, no review_* calls.fields, can still cross the threshold.get_budgets_live (live) / get_budgets (cache). Use them to inform spending flags — if a category has a budget, flag when spending (2.2) exceeds or approaches it, not just when it exceeds the 90-day average. The amount Copilot returns for this month has rollover applied (in live mode it is budget_amount on get_categories_live (live)), so it is not the figure the user set: it can be far above it, or negative after an overspent month. Never phrase it as "your $X budget". Say what is left ("$20 left in Groceries this month"), and skip a budget whose amount is zero or negative rather than reporting it.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.