Skip to main content

Why scan in CI?

Every unprotected wallet call is a potential unvalidated transaction in production. The Mandate scanner catches these gaps at the code level, before deployment. It exits with code 1 if it finds calls missing Mandate validation, which fails the build and blocks the merge. Running the scanner in CI means no developer can accidentally ship a sendTransaction() or transfer() call without a corresponding validate(). This is your last line of defense before code reaches production.

GitHub Actions

Add the scanner as a step in your existing workflow or create a dedicated security scan job. The scanner requires no authentication and no configuration.
name: Mandate Security Scan
on: [push, pull_request]

jobs:
  mandate-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Scan for unprotected calls
        run: npx @mandate.md/cli scan ./src
The job fails if any unprotected wallet call is detected. Add --json to pipe structured output into downstream steps or artifact uploads.

GitLab CI

Add a scan stage to your .gitlab-ci.yml. The merge_request_event rule ensures the scan runs on every merge request.
mandate-scan:
  image: node:20
  script:
    - npx @mandate.md/cli scan ./src
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
For monorepos, point the scanner at the specific directory containing agent code. You can run multiple scan steps targeting different paths.

Pre-commit hook

Catch unprotected calls before they even enter version control. Add this hook to .git/hooks/pre-commit or use a framework like Husky.
#!/bin/sh
# .git/hooks/pre-commit
npx @mandate.md/cli scan ./src || {
  echo "Unprotected wallet calls found. Run 'npx @mandate.md/cli scan' for details."
  exit 1
}
Make the hook executable with chmod +x .git/hooks/pre-commit. For team-wide enforcement, commit the hook via Husky or simple-git-hooks in your package.json.

What does the scanner check?

The scanner detects 10 financial call patterns across .ts, .js, .tsx, and .jsx files:
PatternDescription
sendTransaction(Generic transaction sends
sendRawTransaction(Raw transaction sends
wallet.transfer(Direct wallet transfers
wallet.send(Shorthand send calls
writeContract(Viem contract writes
walletClient.writeViem wallet client writes
executeAction(...transfer)Framework action executions
execute_swapSwap execution functions
execute_tradeTrade execution functions
A call is marked protected if the file imports from @mandate, references MandateClient, MandateWallet, mandate.validate, or mandate.preflight. Project-level protection applies when @mandate.md/sdk appears in any package.json or a MANDATE.md file exists at the root.

How do you fix findings?

Wrap each unprotected transaction call with a validate() call in the same function scope. The minimal fix:
import { MandateClient } from '@mandate.md/sdk';

const client = new MandateClient({ runtimeKey: process.env.MANDATE_RUNTIME_KEY });

// Before: unprotected
await wallet.sendTransaction({ to, value, data });

// After: protected
await client.validate({ action: 'transfer', to, amount, reason: 'Payment for service' });
await wallet.sendTransaction({ to, value, data });
For high-level flows, use MandateWallet which handles validation, signing, and event reporting in a single call. See Validate Transactions for the full pattern.
Run the scanner with --verbose locally to see both protected and unprotected calls. This gives you a complete picture of all financial operations in your codebase.

Next Steps

Codebase Scanner Guide

Detailed walkthrough of scanner patterns, ignore rules, and remediation steps.

Scan CLI Reference

Full flag reference, exit codes, and output formats for the scan command.

Validate Transactions

Add validation to the unprotected calls the scanner found.