* chore: split AGENTS.md across the monorepo to fix size warning Root AGENTS.md was 41.7k chars, exceeding agent's 40k performance threshold. Per the agents.md spec for monorepos, split into nested files that agents auto-load from each subtree: - apps/web/AGENTS.md (new): feature architecture, Storybook, web testing, web-specific pitfalls, debugging tips - apps/mobile/AGENTS.md (new): Expo + Tamagui guidance - packages/AGENTS.md (new): shared-package conventions, dual env vars - apps/web/docs/code-style.md: appended Code Complexity Guidelines - apps/mobile/docs/code-style.md: cross-reference to complexity rules - root AGENTS.md: trimmed to cross-cutting rules + Nested guidance index Root drops 41.7k -> 23.8k chars (no warning, ~16k headroom). All content preserved in the new layout; existing nested files (cypress, .storybook) unchanged. README, CLAUDE.md, PR template, and specs/** still reference the (still-existing) root file. * chore: gitignore docs/brainstorms/ like docs/plans/ Brainstorms are local working artifacts, same as plans. Remove the accidentally-committed brainstorm file and prevent future leaks.
23 KiB
AI Contributor Guidelines
This repository is the Safe{Wallet} monorepo, containing both web and mobile applications for Safe (formerly Gnosis Safe), a multi-signature smart contract wallet on Ethereum and other EVM chains. The repository uses a Yarn 4 workspace-based monorepo structure. Follow these rules when proposing changes via an AI agent.
Nested guidance
This monorepo uses nested AGENTS.md files. Agents working in a subtree automatically load the nearest one. Start at root for cross-cutting rules, then drop into the relevant subtree:
| Subtree | File | Covers |
|---|---|---|
apps/web/ |
apps/web/AGENTS.md | Feature architecture, Storybook, web testing, web pitfalls |
apps/web/cypress/ |
apps/web/cypress/AGENTS.md | Cypress E2E patterns |
apps/web/.storybook/ |
apps/web/.storybook/AGENTS.md | Storybook fixtures and provider patterns |
apps/mobile/ |
apps/mobile/AGENTS.md | Expo + Tamagui |
packages/ |
packages/AGENTS.md | Shared packages, dual env vars |
When adding new guidance, place it in the most-specific subtree it applies to.
Quick Start
Common commands for getting started:
# Install dependencies (uses Yarn 4 via corepack)
yarn install
# Run web app in development mode
yarn workspace @safe-global/web dev
# Run mobile app in development mode
yarn workspace @safe-global/mobile start
# Run tests for web
yarn workspace @safe-global/web test
# Run Storybook for web
yarn workspace @safe-global/web storybook
Turborepo
Root-level lint, type-check, and test run through Turborepo. Tasks are cached by input hash and re-used on subsequent runs — locally and in CI.
yarn type-check # all workspaces (cached)
yarn turbo run type-check --filter=@safe-global/web # scoped
yarn turbo run test --filter=@safe-global/utils... # package + dependents
Cache directory is .turbo/ (gitignored). Task definitions live in turbo.json.
Remote cache
CI reads TURBO_TOKEN (repo secret) and TURBO_TEAM (repo variable) via .github/actions/yarn. These must be configured once per Vercel team:
- Create or pick a Vercel team; copy the team slug → set repo variable
TURBO_TEAM. - Create a Vercel personal access token with access to that team → set repo secret
TURBO_TOKEN. - Locally:
yarn turbo login && yarn turbo linkto enable remote cache in development.
Self-hosted cache (e.g. ducktors/turborepo-remote-cache) can be wired by setting TURBO_API, TURBO_TOKEN, TURBO_TEAM — the same env vars the Vercel backend uses.
Architecture Overview
- apps/web - Next.js web application
- apps/mobile - Expo/React Native mobile application
- packages/ - Shared libraries (store, utils, etc.) used by both platforms
- config/ - Shared configuration files
The monorepo uses Yarn 4 workspaces to manage dependencies and enables sharing code between web and mobile applications.
Key Entry Points
Stable architectural landmarks for fast orientation:
| Area | Path | Purpose |
|---|---|---|
| Web app entry | apps/web/src/pages/_app.tsx |
Next.js app bootstrap, providers, InitApp |
| Redux store | apps/web/src/store/index.ts |
makeStore(), middleware, RTK Query APIs |
| RTK Query APIs | apps/web/src/store/api/gateway/ |
CGW API endpoints (balances, transactions, etc.) |
| Feature system | apps/web/src/features/__core__/ |
createFeatureHandle, useLoadFeature, proxy stubs |
| Page layout | apps/web/src/components/common/PageLayout/ |
Main app layout, sidebar, header |
| Safe info hook | apps/web/src/hooks/useSafeInfo.ts |
Current Safe address, owners, threshold |
| Chain config | packages/store/src/gateway/chains/ |
RTK Query chains endpoint with retry logic |
| Theme package | packages/theme/src/ |
Palettes, spacing, typography tokens |
| Mobile entry | apps/mobile/src/app/_layout.tsx |
Expo Router root layout |
AST-Based Code Search
If ast-grep (aka sg) is installed, prefer it over text-based grep for structural code searches. It understands TypeScript/TSX syntax so it won't match inside comments or strings.
# Find all components using useAppSelector
sg -p 'useAppSelector($$$)' --lang tsx apps/web/src/
# Find all createSlice calls
sg -p 'createSlice({ name: $NAME, $$$})' --lang ts apps/web/src/
# Find all default exports of a function component
sg -p 'export default function $NAME($$$) { $$$}' --lang tsx apps/web/src/
# Find useMemo with specific dependency
sg -p 'useMemo(() => $$$, [$$$, chainId, $$$])' --lang tsx apps/web/src/
Install: brew install ast-grep or npm install -g @ast-grep/cli
TypeScript LSP (symbol-aware navigation)
When available in your agent environment, the LSP tool exposes the TypeScript language server and indexes the entire monorepo (apps/ + packages/, ~40k+ symbols). Use it for any question about what a symbol is or who uses it — it follows imports, re-exports, and module resolution, and ignores matches in comments and strings. This is strictly more accurate than grep for symbol-level questions, and complements ast-grep (which is best for structural pattern matching).
When to reach for LSP (strongly preferred over grep):
- "Who consumes this hook / component / selector / slice / endpoint / type?" →
findReferences - "Where is this symbol defined?" →
goToDefinition - "What are all implementations of this interface?" →
goToImplementation - "What's the exported API of this file?" →
documentSymbol - "Does a symbol named X exist anywhere, and where?" →
workspaceSymbol - "Who calls this function, and whom does it call?" →
prepareCallHierarchy+incomingCalls/outgoingCalls - "What type is this expression?" →
hover
When to reach for ast-grep instead:
- Structural patterns, not symbol identity. E.g. "every
useMemowhose deps array containschainId", "every call tocreateSlicewith a given shape".
When plain grep is still fine:
- Searching strings, comments, config, copy/UI text, file names, or anything that isn't a TS/TSX identifier.
Gotcha — default exports: For files that use export default, target the identifier on the export default line, not the local const/function binding, or you will miss all the importing consumers. Example: for apps/web/src/hooks/useSafeInfo.ts, aiming findReferences at the local const useSafeInfo binding returns ~2 refs; aiming at export default useSafeInfo returns 500+ refs across the codebase. For named exports this does not matter.
All operations take: filePath, line (1-based), character (1-based), operation.
# Examples
operation=documentSymbol filePath=apps/web/src/hooks/useSafeInfo.ts line=1 character=1
operation=findReferences filePath=apps/web/src/hooks/useSafeInfo.ts line=29 character=16 # the "default" identifier
operation=goToDefinition filePath=apps/web/src/hooks/useSafeInfo.ts line=4 character=10 # selectSafeInfo import
operation=workspaceSymbol filePath=<any .ts file> line=1 character=1 # index-wide symbol search
Cost note: workspaceSymbol with an empty/broad query returns tens of thousands of entries (1.7 MB+) and will be truncated to a persisted file — use it with a specific query, or prefer findReferences / goToDefinition starting from a known site.
Unified Theme System
The project uses @safe-global/theme package as a single source of truth for all design tokens (colors, spacing, typography, radius) across web and mobile.
Key Features
- Unified Palettes: Light and dark mode color palettes shared between platforms
- Dual Spacing Systems: 4px base for mobile, 8px base for web (with overlapping values using same names)
- Platform Generators: Automatic generation of MUI themes (web) and Tamagui tokens (mobile)
- Static Colors: Theme-independent brand colors available to both platforms
Usage
Web (MUI):
import { generateMuiTheme } from '@safe-global/theme'
const theme = generateMuiTheme('light') // or 'dark'
Mobile (Tamagui):
import { generateTamaguiTokens, generateTamaguiThemes } from '@safe-global/theme'
const tokens = generateTamaguiTokens()
const themes = generateTamaguiThemes()
Direct Token Access:
import { lightPalette, darkPalette, spacingMobile, spacingWeb, typography } from '@safe-global/theme'
Modifying Theme
To add or modify colors/tokens:
- Edit files in
packages/theme/src/palettes/orpackages/theme/src/tokens/ - Run type-check to ensure consistency:
yarn workspace @safe-global/theme type-check - Regenerate CSS vars for web:
yarn workspace @safe-global/web css-vars
Important Notes
- Never edit
apps/web/src/styles/vars.cssdirectly - it's auto-generated - Always use theme tokens instead of hard-coded colors
- Both light and dark modes must be updated together for consistency
General Principles
- Follow the DRY principle – avoid code duplication by extracting reusable functions, hooks, and components
- Prefer functional code over imperative – use pure functions, avoid side effects, leverage
map/filter/reduceinstead of loops - Use declarative and reactive patterns – prefer React hooks, derived state, and data transformations over manual state synchronization
- Always cover new logic, services, and hooks with unit tests
- Run type-check, lint, prettier and unit tests before each commit
- Never use the
anytype! - Treat code comments as tech debt! Add them only when really necessary & the code at hand is hard to understand.
- Use sentence case for UI text – Buttons, headings, labels, warnings, and other UI copy should use sentence case (e.g., "Add new owner") not Title Case (e.g., "Add New Owner")
Web-specific principles (feature architecture, MUI theme, vars.css, feature flags, Storybook story requirement) live in apps/web/AGENTS.md. Mobile-specific principles live in apps/mobile/AGENTS.md.
Testing Requirements
Every code change must include tests. See apps/web/docs/TESTING.md for conventions, templates, and mock patterns.
Workflow
Fast Feedback Loop
The repo provides automated verification:
-
Automatic: A Claude Code
Stophook runsverify:changedonce at the end of each agent turn. It early-exits (no-op) when no.ts/.tsx/.js/.jsxfiles have been modified. When it does run, type-check runs on the full project (TSC requires this), while lint, prettier, and tests are scoped to changed files only. The workspace (web/mobile) is auto-detected from the changed file paths. SetSKIP_VERIFY=1to disable. Fix any errors before moving on. -
Manual: Run
yarn verify:changed:webanytime to check your work. Runyarn verify:webfor a full check before committing. -
Test scaffolding: Run
yarn test:scaffold <file>to generate a test skeleton with the correct imports, mocks, and structure. See the Test Decision Matrix in the Testing Guidelines section for which files need tests.
Rules for agents:
- Fix all
verify:changederrors before proceeding to the next task - If
verify:changedreports a missing test, write one before committing - Do NOT run type-check, lint, prettier, and test separately — use
verify - Do NOT commit without a clean
verify:changedpass
Pre-implementation regression checklist (REQUIRED)
Before writing code for any non-trivial change (anything beyond a typo, doc tweak, or single-line local fix), you MUST produce a regression checklist and include it in your response to the user. Optimise for impact analysis, not diff completion: a change to a shared hook, selector, component, slice, or API endpoint touches many user journeys, and plain text search is not enough to find them.
Build the checklist in this order:
- Map the surface. Identify what you are touching: the primary file(s), plus any shared hooks, components, selectors, Redux slices, RTK Query endpoints, feature flags, routes, or persisted state involved.
- Find consumers with symbol-aware search. For "who uses this symbol?" questions, prefer the
LSPtool'sfindReferences(see "TypeScript LSP" above) — it follows imports, re-exports, and module resolution across the whole monorepo. Useast-grepfor structural pattern questions (e.g. "everyuseMemowithchainIdin deps"). Fall back to plaingreponly for strings, comments, config, or UI copy. Plain text search misses structural usages, matches inside comments/strings, and does not follow re-exports. - Translate consumers into flows. For each consumer, name the user journey it belongs to (create / edit / delete / retry / empty / error / offline / permission / feature-flag-off / mobile variant).
- List tests to add or run. Happy path, each neighbouring flow, regression-sensitive paths, and invariant properties. Prefer targeted tests around shared contracts over broad E2E sweeps.
- State what you will NOT verify. Be explicit. This exposes false confidence.
Required checklist format (paste into your response before implementing):
### Regression checklist
**Primary flow changed:** <one sentence>
**Surfaces touched:**
- <shared hook / component / selector / slice / endpoint / flag / route>
**Neighbouring flows to verify:**
- <flow A> — <why it could be affected>
- <flow B> — <why it could be affected>
**Tests to add/run:**
- <test name or description>
**Not verified (risks):**
- <what you are skipping and why>
Rules:
- Do NOT start editing code until this checklist exists in the conversation. For small, strictly local changes, a one-line "local change, no shared surfaces touched" note is sufficient.
- When you open the PR, carry the relevant lines into the "Affected flows", "Blast radius", and "Risks / not checked" fields of the PR template.
- If the checklist reveals that a shared abstraction has many unknown consumers, slow down and investigate before coding — that is the signal this process is designed to surface.
-
Install dependencies:
yarn install(from the repository root).- Uses Yarn 4 (managed via
corepack) - Automatically runs
yarn after-installfor the web workspace, which generates TypeScript types from contract ABIs
- Uses Yarn 4 (managed via
-
Pre-commit hooks: The repository uses Husky for git hooks:
- pre-commit: Automatically runs
lint-staged(prettier) and type-check on staged TypeScript files - pre-push: Runs linting before pushing
- These hooks ensure code quality before commits reach the repository
- If hooks fail: Fix the reported issues and try committing again. Common issues:
- Type errors: Run
yarn workspace @safe-global/web type-checkto see all errors - Formatting: Run
yarn prettier:fixto auto-fix - Linting: Run
yarn workspace @safe-global/web lint:fixto auto-fix where possible
- Type errors: Run
- pre-commit: Automatically runs
-
Formatting (CRITICAL): ALWAYS run
yarn prettier:fixbefore staging and committing. Do NOT rely on lint-staged alone — it can miss formatting issues due to stash/restore edge cases. Run it explicitly:yarn prettier:fixThen verify with
yarn workspace @safe-global/web prettier(the check-only command). CI will reject unformatted code. -
Linting and tests: when you change any source code under
apps/orpackages/, execute, for web:yarn workspace @safe-global/web type-check yarn workspace @safe-global/web lint yarn workspace @safe-global/web prettier # verify formatting (CI runs this) yarn workspace @safe-global/web testFor mobile:
yarn workspace @safe-global/mobile type-check yarn workspace @safe-global/mobile lint yarn workspace @safe-global/mobile prettier yarn workspace @safe-global/mobile test -
Commit messages: use semantic commit messages as described in
CONTRIBUTING.md.- Examples:
feat: add transaction history,fix: resolve wallet connection bug,refactor: simplify address validation - CI/CD changes: Always use
chore:prefix for CI, workflows, build configs (NEVERfeat:orfix:) - Test changes: Always use
tests:prefix for changes in unit or e2e tests (NEVERfeat:orfix:)
- Examples:
-
Code style: follow the guidelines in:
apps/web/docs/code-style.mdfor the web app.apps/mobile/docs/code-style.mdfor the mobile app.
-
Pull requests: fill out the PR template and ensure all checks pass.
-
PR poem: Include a short technical poem at the very top of each PR description that acts as a concise summary of what the PR actually changes. The poem should prioritize clarity over artistry — a reader should understand the gist of the PR from the poem alone. Use a randomly chosen short form (e.g., haiku, limerick, free verse, tanka) and keep it to 2–4 lines. Wrap in a blockquote:
> Strip Sentry SDK and config, > no more error tracking calls, > bundle shrinks, tests pass clean. -
PR description: Always use the GitHub PR template (
.github/PULL_REQUEST_TEMPLATE.md). Fill out all sections — "What it solves", "How this PR fixes it", "How to test it", and the checklist. -
PR visual summary (required): Every PR must include a visual in the
## Visual summarysection. This is mandatory, not optional.- Architecture/logic changes → Mermaid diagram (flowchart, sequence, or class diagram) showing what changed
- UI changes → Screenshot of the result (use Chrome DevTools MCP if the app is running, or describe how to capture manually)
- Both if the PR includes UI + logic changes
Mermaid diagrams are rendered natively by GitHub. Example:
```mermaid flowchart LR A[useSafeInfo hook] --> B[New validation logic] B --> C{Is owner?} C -->|Yes| D[Show actions] C -->|No| E[Show read-only] ```For refactors, use a before/after diagram:
```mermaid flowchart TB subgraph Before A1[Component A] --> B1[Inline logic] A1 --> C1[Inline logic] end subgraph After A2[Component A] --> H[useSharedHook] H --> B2[Extracted service] end ```
Environment Variables – Web apps use NEXT_PUBLIC_* prefix, mobile apps use EXPO_PUBLIC_* prefix for environment variables. In shared packages, check for both prefixes.
Testing Guidelines
Unit Tests
-
When writing Redux tests, verify resulting state changes rather than checking that specific actions were dispatched.
-
Avoid
anytype assertions – Create properly typed test helpers instead of usingas any. For example, when testing Redux slices with a minimal store, create a helper function that properly types the state:// Good: Properly typed helper type TestRootState = ReturnType<ReturnType<typeof createTestStore>['getState']> const getSafeState = (state: TestRootState, chainId: string, safeAddress: string) => { return state[sliceName][`${chainId}:${safeAddress}`] } // Bad: Using 'any' const state = store.getState() as any -
Use Mock Service Worker (MSW) for tests involving network requests instead of mocking
fetch. Use MSW for mocking blockchain RPC calls instead of mocking ethers.js directly -
Create test data with helpers using faker
-
Ensure shared package tests work for both web and mobile environments
-
Test files should be colocated with source files using the
*.test.ts(x)naming convention
Platform-specific testing
- Web (Cypress E2E, test-coverage commands, test decision matrix, what NOT to test): see apps/web/AGENTS.md.
- Mobile (E2E guidelines, mobile test commands): see apps/mobile/AGENTS.md.
Security & Safe Wallet Patterns
Safe (formerly Gnosis Safe) is a multi-signature smart contract wallet that requires multiple signatures to execute transactions.
Key Concepts
- Safe Account – A smart contract wallet requiring M-of-N signatures to execute transactions
- Owners – Addresses that can sign transactions for a Safe
- Threshold – Minimum number of signatures required to execute a transaction
- Transaction Building – Follow Safe SDK patterns for building multi-signature transactions using
@safe-global/protocol-kit
Best Practices
- Safe Address Validation – Always validate Ethereum addresses using established utilities (ethers.js
isAddress) - Chain-Specific Safes – Safe addresses are unique per chain; always include chainId when referencing a Safe
- Transaction Building – Use the Safe SDK (
@safe-global/protocol-kit,@safe-global/api-kit) for transaction creation - Wallet Provider Integration – Follow established patterns for wallet connection and Web3 provider setup (Web3-Onboard)
- Never hardcode private keys or sensitive data – Use environment variables and secure key management
Environment Configuration
- Local Development – Points to staging backend by default
- Environment Branches – PRs get deployed automatically for testing
- RPC Configuration – Infura integration for Web3 RPC calls (requires
INFURA_TOKEN) - Chain Configuration – Chain configs are managed through the Safe Config Service
Common Pitfalls
Cross-cutting mistakes to avoid. Web-specific pitfalls live in apps/web/AGENTS.md; mobile-specific ones in apps/mobile/AGENTS.md.
- Using
anytype – Always properly type your code, create interfaces/types as needed. - Forgetting to run tests – Always run tests before committing.
- Breaking mobile when changing shared code – Shared packages (
packages/**) affect both web and mobile. - Modifying generated files – Never manually edit auto-generated files in
packages/utils/src/types/contracts/orpackages/store/src/gateway/AUTO_GENERATED/. CI fails if they don't match the schema. Runyarn workspace @safe-global/store build:devto regenerate the store types. - Not handling chain-specific logic – Always consider multi-chain scenarios.
- Incomplete error handling – Always handle loading, error, and empty states in UI components.