finance-cleanup — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited finance-cleanup (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
A bulleted imperative like {match} tells the agent to never reveal, disclose, or mention something to the user. Used adversarially it can instruct the agent to hide its tool calls or lie about what it did — stripping the transparency a user relies on to trust the agent.
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-pulse/finance/amazon-syncupdate_transaction, create_recurring, set_recurring_state, update_recurring, review_transactions, create_category, update_category, or delete_category call, show the user the proposed change (current value → new value) and wait for explicit confirmation. The Phase-3 confident-fix carve-out (merchant's dominant category >80%, or explicit profile rule) still applies — those can apply without per-item confirmation but must still be reported afterward.Walk the user through a structured cleanup of their Copilot Money transaction data. You have access to MCP tools for reading and writing Copilot Money data. This is a multi-phase process: structural audit, gather, detect, present, fix, update profile, summarize.
Do this BEFORE any transaction-level work. Transaction cleanup lands in the right buckets only if the taxonomy itself makes sense. Skipping this turns miscategorization fixes into busywork.
--write implies --live-reads, so the server is in live mode and every read below goes to Copilot's API. If the session has already read transactions or categories (or the user says the data looks stale), call refresh_cache once at the very start of Phase 0, not Phase 1: Phase 0 already reads categories + 6 months of transactions, so stale reads would surface structural problems that don't exist (or hide new ones). One refresh covers both phases.get_categories_live for the full tree (each row's parentId gives the hierarchy), then get_transactions_live over the last 6 months to compute per-category spend and per-category transaction count. The default terse row is enough for this pass — amount, category_name and date are all in it. For this aggregation pass, paginate through multiple `get_transactions_live` calls (100 rows per page, limit and offset) — you need the full 6-month set for accurate category totals, not a one-page sample. Aggregate per-category counts and sums incrementally with Python via Bash as each page comes back.create_category, update_category, delete_category as approved.Any refresh happened in Phase 0 Step 0 — do not refresh again. Mid-session refreshes make earlier results disagree with later ones and force re-pulls. One refresh up front covers the whole session.
~/.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 any existing preferences, especially under "Cleanup Preferences," "Preferences," and "Recurring Matcher State." These override your judgment — if the profile says "Uber Eats = Dining," never flag Uber Eats as miscategorized.get_transactions_live with fields: ["default", "user_reviewed", "normalized_merchant"] — last 6 months of all transactions (for historical patterns), and the same call is how you find the unreviewed ones: there is no reviewed filter argument, so pull the window and filter on user_reviewed === false in Python. The fields argument is required for Phase 2's passes: rows are terse by default, and user_reviewed (the reviewed/unreviewed split) and normalized_merchant (the merchant grouping in Phase 2.1) are not in the default row. Live rows have no original_name; the full merchant string every presentation rule below demands is name.get_recurring_live — current recurring chargesget_categories_live — full category listget_accounts_live — to map account IDs to names (the account's key is id)`limit=100` is fine now. Rows went terse by default in v3.0.0, so a 100-row page measures ~28KB against a real cache (it was ~120KB, which is why this used to say limit=50 and warn about the 100KB response cap). The three extra fields this skill asks for above add a little back; if a page still spills to disk, drop to limit=50 rather than dropping the fields.
Run all reads before any analysis. Cache the results mentally — you will cross-reference heavily.
Work through each detection pass. Use Bash with Python for any arithmetic on large transaction sets (aggregations, frequency analysis, statistical checks). Do not attempt mental math on more than ~10 numbers.
Run two passes. The first catches the highest-signal cases; the second sweeps up the rest.
Pass A — Cross-category merchants (do this FIRST).
Group all transactions by normalized_merchant over the 6-month history (it is in the Phase 1 pull's fields list — it is not in the default row). For each merchant, count the number of distinct category_name values used. Flag every merchant with ≥2 distinct categories as a high-confidence miscategorization candidate — when the same merchant has been filed under multiple buckets, at least one of those categorizations is almost certainly wrong.
Threshold notes:
Pass B — Dominant-category drift.
For each merchant that appears 3+ times in the 6-month history AND wasn't already surfaced in Pass A:
Exceptions (apply to both passes):
Payment app accounts (Venmo, PayPal, etc.): These create two-sided transactions — a generic stub on the bank/checking side (e.g., "VENMO" with no detail, marked as internal transfer) and a richly-described transaction on the payment app account side (e.g., "Bar Whistler to Sam Rivera" with proper category). Before flagging bank-side payment app stubs as false transfers, check if the payment app account exists (get_accounts_live) and whether the real transaction lives there. If both sides exist, the bank-side stub is correctly marked as a transfer — leave it alone.
Two directions to check:
False transfers (marked Internal Transfer but probably not):
Missing transfers (should be Internal Transfer but are not):
Scan the 6-month transaction history for merchants appearing 3+ times at regular intervals:
get_recurring_live. Skip merchants already tracked.When get_recurring_live surfaces a sub as "overdue" (its nextPaymentDate has passed and the charge hasn't arrived), do not recommend archiving it until you've ruled out a stale matcher rule. In real sessions, ~⅔ of "overdue" subs were actually still charging — the matcher's name-match or amount-bound rule had just drifted.
Before flagging archive, for each overdue sub:
get_recurring_live with fields: ["default", "rule"] and find the sub by name or id. The matcher config lives in rule (nameContains / minAmount / maxAmount) and is not in the default row; without the fields argument there is no rule to read and every overdue sub looks like a dead one.~/.claude/copilot-money/user-profile.md — if the sub is listed there with known oddities (e.g. "semi-annual, Copilot next_date is buggy for this one"), honor it and skip.get_transactions_live with merchant: "<approximate name>" and period: "last_90_days". Watch for the two common drift modes:name_contains: "Servicename" but the merchant posts as SERVICENAM (payment processor truncated it) — never matches.max_amount.update_recurring to fix the rule — widen the amount range, correct the name substring, or both. Only propose set_recurring_state to archive if the sub is genuinely silent for 30+ days past the expected `next_date` with zero near-miss matches in the last 90 days. (Phase 2.3 flags missed cycles at 7+ days, which is right for "might be cancelled" — but archiving is a heavier call and should wait for a full extra cycle to rule out delayed posting.)When a matcher is updated, write the new rule back to the user profile's "Recurring Matcher State" section (see Phase 4 step 2) so next session doesn't re-investigate the same sub from scratch.
Tone: Be blunt and direct. Talk like a friend reviewing a bank statement, not a financial advisor writing a report.
Examples of good phrasing:
Presentation rules:
Transaction presentation format: When showing a transaction to the user, always include:
name (NOT the truncated normalized_merchant — users need the full text to recall context, e.g., "ENC *DOCTOR NAME C.SANTIAGO" not "ENC")Do NOT use AskUserQuestion for large batches. The interactive question tool is too slow when there are 10+ items needing decisions. Instead:
Never auto-approve. Never skip the presentation phase.
Phase 2 built an in-memory list of all findings. Phase 3 presents a batch from that list and waits for user approval. Phase 4 then runs the loop below for each approved batch — and loops back to Phase 3 with the remaining findings until the list is empty or the user stops.
Per-batch loop:
edits entry per transaction (max 200; chunk beyond that). Those four fields are not bulk-editable by Copilot, so they can only go through this path. It fans out one request per row but still costs a single agent turn.update_transaction — that costs one agent turn per row, which is how a routine cleanup turns into an hours-long run that exhausts the context budget.review_transactions for cleaned-up transactions (bulk; takes an array).create_recurring per merchant the user confirms as recurring.update_recurring per Phase 2.4.Bulk-write rules:
update_transactions call — chunk anything larger.category_id anywhere fails the whole call before any write lands. Fix the entry and re-issue — nothing was half-applied.continue_on_error: true for backlog sweeps where a few unwritable rows (e.g. outside the 13-month write window) shouldn't strand the rest; read failures[] and report them. Leave it off when the batch is a coherent unit.update_transaction calls for genuine one-offs.update_recurring succeeded → append the new rule to the "Recurring Matcher State" section of ~/.claude/copilot-money/user-profile.md.update_transaction recategorized a recurring merchant (appears 3+ times in the 6-month transaction history) → append a merchant→category mapping to the "Cleanup Preferences" section.delete_category / update_category (archive or rename) → note it under "Cleanup Preferences" so the next session knows.Why a mutable list, not a re-pull: a re-pull would re-run the Phase 2 heuristics over data the user has just reviewed and turn up findings they already declined. It would also agree with what was just written: this MCP server keeps a snapshot cache of live reads, each write patches or invalidates the month(s) it touched, and a re-pull refetches only those months and serves the rest from the cache. So freshness is not the problem; re-surfacing declined findings is. The Phase 2 snapshot, pruned as items are fixed or skipped, is the list the user is working through.
The bulk of profile updates happen incrementally in Phase 4 (per the C3 rule). Phase 5 is the catch-all for things that don't tie to a specific Phase-4 batch.
"Profile updates this session: [N] matcher rules, [M] merchant mappings, [K] skip preferences."
The user should see, in one line, what their profile gained — so they know the work compounds across sessions.
Reference — what gets saved where (handled inline in Phase 4, repeated here for the reader):
| Trigger | Profile section |
|---|---|
update_recurring succeeds | Recurring Matcher State |
update_transaction on a recurring merchant | Cleanup Preferences (merchant→category) |
delete_category / update_category | Cleanup Preferences (note) |
| Account-skip preference stated | Cleanup Preferences |
| Category-never-touch preference stated | Cleanup Preferences |
Recurring Matcher State format (per entry):
- <Display name>
- Merchant substring: `<exact text the processor posts>`
- Amount range: $min – $max
- Oddities: <e.g. "semi-annual; Copilot next_date is buggy">End with a brief summary:
Closing note: Always close with:
"Your fixes are live in Copilot Money. If the app still shows an old value, refresh it; the change is already saved."
This pre-empts the confusion when an open app tab still shows the value from before the fix.
~/.claude/copilot-money/user-profile.md preferences override statistical analysis. If the profile says a merchant is categorized a certain way, do not flag it.name field — not the truncated normalized_merchant. Users need the full text to recall what a transaction was (e.g., "ENC *DOCTOR NAME C.SANTIAGO" is identifiable, "ENC" is not).update_transaction, only user-created category IDs work (e.g., 5Qqr8qs3GHNCj8H6fIKd). Plaid taxonomy IDs (e.g., general_services, food_and_drink_restaurant) will fail. If the needed category doesn't exist, use create_category first, then use the returned ID.pending_transaction_id. Do not cache or reference transaction IDs across sessions — always re-query.fields can still cross the threshold.get_transactions_live hides internal transfers by default. To analyze transfers (misclassified transfer detection), pass exclude_transfers: false.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.