(value: T): T { ... }
+```
+
+Common utility types in use: `Pick<>` (30 files), `Omit<>` (40 files),
+`Partial<>` (30 files), `Record<>` (15 files), `keyof`/`typeof` (167 occurrences
+in 71 files).
+
+## TypeScript Strictness
+
+- `any` exists in ~330 occurrences (mostly test files). Avoid in production
+ code. Use `unknown` instead.
+- `unknown` has low adoption (41 occurrences) — prefer it over `any` for new
+ code
+- `as unknown as Type` double-cast: only 13 occurrences (acceptable in tests
+ only)
+
+## CSS/SCSS Patterns
+
+110 SCSS files using BEM naming with kebab-case:
+
+```scss
+.AccountDetail {
+ &__wrapper { ... }
+ &__balance-info { ... }
+ &__action-button { ... }
+}
+```
+
+Design system tokens: use `var(--sds-clr-*)` CSS variables and `pxToRem()` mixin
+from `styles/utils.scss`. Never use raw pixel values or hardcoded colors.
+
+## Test File Patterns
+
+- File naming: always `.test.ts` / `.test.tsx` (never `.spec.ts`)
+- Structure: `describe("feature")` + `it("should...")` (universal — 922 test
+ cases across 95 files)
+- Setup: `beforeEach` in 88 of 95 test files (93%)
+- Assertions: `expect().toBe()` for primitives, `expect().toEqual()` for objects
+
+## Comments
+
+Comments are intentionally sparse (~2-5 per 500 lines). Don't add unnecessary
+comments.
+
+- **JSDoc:** Only on exported types/interfaces in shared packages
+- **Inline comments:** Only for non-obvious logic
+- **TODOs:** Must reference a GitHub issue (`// TODO(#1234): description`)
+- **No license headers** — files begin directly with imports
diff --git a/docs/skills/freighter-best-practices/references/dependencies.md b/docs/skills/freighter-best-practices/references/dependencies.md
new file mode 100644
index 0000000000..c134c3b88e
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/dependencies.md
@@ -0,0 +1,116 @@
+# Dependencies -- Freighter Extension
+
+## Package Manager
+
+- **Yarn 4.10.0** with workspaces enabled
+- Node requirement: **>= 22** (specified in `.nvmrc`)
+- Always use `nvm use` or equivalent before running commands to ensure the
+ correct Node version
+
+## Workspace Structure
+
+The root `package.json` defines 6 workspaces:
+
+```json
+{
+ "workspaces": [
+ "extension",
+ "@stellar/freighter-api",
+ "@shared/api",
+ "@shared/constants",
+ "@shared/helpers",
+ "docs"
+ ]
+}
+```
+
+## Dependency Placement
+
+| Dependency Type | Where to Add | Example |
+| -------------------------- | ------------------------------------- | ----------------------------------------- |
+| Extension runtime deps | `extension/package.json` | `react`, `redux`, `stellar-sdk` |
+| freighter-api runtime deps | `@stellar/freighter-api/package.json` | Minimal, SDK-only deps |
+| Shared module deps | `@shared/*/package.json` | Shared utilities |
+| Dev tooling (shared) | Root `package.json` | `typescript`, `eslint`, `webpack`, `jest` |
+| Docs | `docs/package.json` | Documentation tooling |
+
+## @shared Modules
+
+Shared packages are imported as workspace dependencies. Yarn resolves them via
+the workspaces configuration:
+
+```json
+// extension/package.json
+{
+ "dependencies": {
+ "@shared/api": "1.0.0",
+ "@shared/constants": "1.0.0",
+ "@shared/helpers": "1.0.0"
+ }
+}
+```
+
+In code, import directly:
+
+```typescript
+import { sendMessageToBackground } from "@shared/api/internal";
+import { SERVICE_TYPES } from "@shared/constants/services";
+```
+
+## Adding Dependencies
+
+1. Identify the correct workspace (see table above)
+2. Add from the workspace directory:
+
+```bash
+cd extension && yarn add some-package
+# or from root:
+yarn workspace extension add some-package
+```
+
+3. For dev dependencies shared across workspaces, add to root:
+
+```bash
+yarn add -D some-dev-tool
+```
+
+## Lavamoat
+
+Lavamoat provides a script allowlist in `package.json` for supply-chain
+security. It restricts which packages can run install scripts (postinstall,
+preinstall, etc.). When adding a package that requires install scripts, you may
+need to update the Lavamoat configuration.
+
+## npmMinimalAgeGate
+
+The `.yarnrc.yml` file includes:
+
+```yaml
+npmMinimalAgeGate: 7d
+```
+
+This means newly published packages must be at least 7 days old before Yarn will
+allow installation. This protects against supply-chain attacks using freshly
+published malicious packages. If you need to install a package published less
+than 7 days ago, you will need to wait or temporarily override this setting
+(with team approval).
+
+## Upgrading Dependencies
+
+1. Run `yarn install` to resolve and update the lockfile
+2. Verify workspace symlinks are intact (`@shared/*` packages resolve correctly)
+3. Build all workspaces -- some depend on others:
+
+```bash
+yarn build # builds all workspaces in dependency order
+```
+
+4. Run the full test suite to verify nothing broke:
+
+```bash
+yarn test:ci
+yarn test:e2e
+```
+
+5. Check for breaking changes in changelogs, especially for `stellar-sdk`,
+ `react`, and `webpack`
diff --git a/docs/skills/freighter-best-practices/references/error-handling.md b/docs/skills/freighter-best-practices/references/error-handling.md
new file mode 100644
index 0000000000..dafffa537b
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/error-handling.md
@@ -0,0 +1,100 @@
+# Error Handling -- Freighter Extension
+
+## Async Thunks
+
+All async thunks must catch errors and use `rejectWithValue` to propagate them
+through Redux:
+
+```typescript
+export const submitTransaction = createAsyncThunk(
+ "transactionSubmission/submit",
+ async (xdr: string, { rejectWithValue }) => {
+ try {
+ const result = await sendTransaction(xdr);
+ return result;
+ } catch (error) {
+ if (error instanceof Error) {
+ return rejectWithValue({ errorMessage: error.message });
+ }
+ return rejectWithValue({ errorMessage: "Unknown error" });
+ }
+ },
+);
+```
+
+Handle rejected cases in extra reducers:
+
+```typescript
+builder.addCase(submitTransaction.rejected, (state, action) => {
+ state.status = ActionStatus.ERROR;
+ state.error = (action.payload as ErrorMessage)?.errorMessage;
+});
+```
+
+## Background Handlers
+
+Background message handlers must never throw. Always return structured response
+objects:
+
+```typescript
+// CORRECT: return result or error objects
+export const handleGetBalance = async (publicKey: string) => {
+ try {
+ const balance = await fetchBalance(publicKey);
+ return { result: balance };
+ } catch (error) {
+ captureException(error);
+ return { error: "Failed to fetch balance" };
+ }
+};
+
+// WRONG: throwing from a handler
+export const handleGetBalance = async (publicKey: string) => {
+ const balance = await fetchBalance(publicKey); // throws on failure
+ return balance;
+};
+```
+
+Use `captureException()` from Sentry for unexpected errors that should be
+tracked.
+
+## ErrorBoundary
+
+The popup wraps its component tree in a class-based `ErrorBoundary` at
+`popup/components/ErrorBoundary/`:
+
+- Implements `getDerivedStateFromError` to catch render errors
+- Implements `componentDidCatch` to report errors to Sentry
+- Displays a fallback UI when a render crash occurs
+- This is the only class component in the codebase -- React does not support
+ error boundaries as functional components
+
+## Network Errors
+
+Horizon transaction submission can return specific error codes that need
+user-friendly mapping:
+
+| Horizon Error Code | Meaning |
+| --------------------- | ------------------------------------------ |
+| `op_underfunded` | Insufficient balance for the operation |
+| `tx_insufficient_fee` | Fee too low for current network conditions |
+| `op_no_destination` | Destination account does not exist |
+
+These are parsed by the `getResultCodes()` helper and mapped to translated
+user-facing messages.
+
+## Error Type Hierarchy
+
+- **`FreighterApiDeclinedError`** -- thrown when the user declines a dApp
+ request (signing, access). This is an expected error, not a bug.
+- **`ErrorMessage` type** -- `{ errorMessage: string }` used as the payload for
+ thunk rejections via `rejectWithValue`.
+
+## Sentry Integration
+
+- `captureException()` for unexpected errors in handlers and async operations
+- Error boundaries catch and report render crashes automatically
+- Do not call `captureException()` for expected errors (user cancellation,
+ invalid input)
+- Never swallow errors silently -- at minimum, report to Sentry if the error is
+ unexpected
diff --git a/docs/skills/freighter-best-practices/references/git-workflow.md b/docs/skills/freighter-best-practices/references/git-workflow.md
new file mode 100644
index 0000000000..1b14a9cd03
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/git-workflow.md
@@ -0,0 +1,76 @@
+# Git & PR Workflow -- Freighter Extension
+
+## Main Branch
+
+The primary branch is `master`. All feature work branches from and merges back
+to `master`.
+
+## Branch Naming
+
+Use descriptive prefixes:
+
+| Prefix | Use Case | Example |
+| ---------- | -------------------------- | ------------------------------ |
+| `feature/` | New functionality | `feature/token-swap-ui` |
+| `fix/` | Bug fixes | `fix/balance-display-rounding` |
+| `chore/` | Maintenance, config, deps | `chore/upgrade-webpack` |
+| `bugfix/` | Alternative bug fix prefix | `bugfix/content-script-race` |
+
+## Commit Messages
+
+- Start with an action verb in present tense: **Add**, **Fix**, **Update**,
+ **Improve**, **Remove**, **Refactor**
+- Keep the subject line concise (under 72 characters)
+- PR number is auto-added by GitHub on squash merge
+
+```
+Add token swap confirmation dialog
+Fix balance rounding for very small amounts
+Update webpack to v5.90
+Refactor background message handler registration
+```
+
+## Pull Request Expectations
+
+- **Focused scope** -- one concern per PR. Split large changes into stacked PRs.
+- **Screenshots** for any UI changes (before/after)
+- **Self-review** -- review your own diff before requesting reviews
+- **Tests** for new features and bug fixes
+- **Cross-browser testing** -- verify in both Chrome and Firefox
+- **Translations updated** -- run `yarn build:extension:translations` and commit
+ locale changes
+
+## CI on Pull Requests
+
+Two workflows run automatically on every PR:
+
+1. **`runTests.yml`** -- runs Jest unit tests and Playwright e2e tests
+2. **`codeql.yml`** -- runs CodeQL security analysis
+
+Both must pass before merging.
+
+## Release Process
+
+Releases are managed by the `newRelease.yml` GitHub Actions workflow:
+
+1. Manual dispatch with `appVersion` parameter (e.g., `5.12.0`)
+2. Workflow creates a release branch and version tag
+3. Bumps `package.json` version and `manifest.json` version
+4. Creates a GitHub release with changelog
+
+## Release Branches
+
+| Branch | Purpose |
+| ------------------- | ------------------------------------------------------------ |
+| `release` | Auto-created by the release workflow for the current release |
+| `emergency-release` | Hotfix branch for critical production issues |
+| `v{X.Y.Z}` | Version tag branches for historical reference |
+
+## Submission Workflows
+
+Separate workflows handle submission to each distribution channel:
+
+- **Chrome Web Store** -- uploads the extension to the Chrome Web Store
+- **Firefox AMO** -- submits to Firefox Add-ons
+- **Safari** -- builds and submits the Safari version
+- **npm** -- publishes `@stellar/freighter-api` to the npm registry
diff --git a/docs/skills/freighter-best-practices/references/i18n.md b/docs/skills/freighter-best-practices/references/i18n.md
new file mode 100644
index 0000000000..d6eb569dd4
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/i18n.md
@@ -0,0 +1,85 @@
+# Internationalization -- Freighter Extension
+
+## Framework
+
+Freighter uses `react-i18next` for internationalization:
+
+- Auto-detects the browser's language setting
+- Falls back to English when a translation is not available
+- String keys use the English text directly, making them greppable in the
+ codebase
+
+## Locale Files
+
+Translation JSON files live in `extension/src/popup/locales/` with one file per
+language:
+
+```
+extension/src/popup/locales/
+ en/
+ translation.json
+ pt/
+ translation.json
+```
+
+## Adding New Strings
+
+1. Wrap the string in `t()` from the `useTranslation` hook:
+
+```typescript
+import { useTranslation } from "react-i18next";
+
+const MyComponent = () => {
+ const { t } = useTranslation();
+ return {t("Send Payment")}
;
+};
+```
+
+2. Add the string to ALL language JSON files (currently English and Portuguese)
+
+3. Run the translation build to verify:
+
+```bash
+yarn build:extension:translations
+```
+
+## Auto-Generation
+
+The command `yarn build:extension:translations` scans the codebase for `t()`
+calls and generates missing keys in locale files. Missing translations default
+to the English text.
+
+## Pre-commit Hook
+
+The translation build runs automatically as a pre-commit hook:
+
+1. Scans for new or modified `t()` calls
+2. Regenerates locale JSON files
+3. Stages the updated locale files for the commit
+
+This ensures translations are never out of sync with the code.
+
+## Testing Translations
+
+To verify translations locally:
+
+1. Change the browser's language setting (e.g., to Portuguese)
+2. Clear `react-i18next` localStorage cache
+3. Refresh the extension popup
+4. Verify all strings display in the target language
+
+## Rules
+
+- Every user-facing string must be wrapped in `t()`
+- Every string must have both English (`en`) and Portuguese (`pt`) translations
+- The custom lint rule `i18n-pt-en` enforces that both translations exist
+- Never use template literals or string concatenation for user-facing text --
+ use `t()` with interpolation instead:
+
+```typescript
+// CORRECT
+t("Send {{amount}} XLM", { amount: "100" });
+
+// WRONG
+`Send ${amount} XLM`;
+```
diff --git a/docs/skills/freighter-best-practices/references/messaging.md b/docs/skills/freighter-best-practices/references/messaging.md
new file mode 100644
index 0000000000..f742de1697
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/messaging.md
@@ -0,0 +1,169 @@
+# Messaging -- Freighter Extension
+
+## Message Flow Architecture
+
+```
+dApp (web page)
+ |
+ | window.postMessage (EXTERNAL_MSG_REQUEST)
+ v
+@stellar/freighter-api (injected SDK)
+ |
+ | window.postMessage
+ v
+Content Script (extension/src/contentScript/)
+ |
+ | chrome.runtime.sendMessage (EXTERNAL_SERVICE_TYPES)
+ v
+Background Service Worker (extension/src/background/)
+ ^ |
+ | | Opens approval window (if needed)
+ | v
+ | Approval Popup
+ | |
+ | chrome.runtime.sendMessage (response)
+ |
+ v
+Popup (extension/src/popup/)
+ |
+ | sendMessageToBackground() --> chrome.runtime.sendMessage (SERVICE_TYPES)
+ v
+Background Service Worker
+```
+
+## Two Message Type Enums
+
+- **`SERVICE_TYPES`** -- messages from the popup to the background (internal
+ extension communication)
+- **`EXTERNAL_SERVICE_TYPES`** -- messages from dApps to the background via the
+ content script (external communication)
+
+Both enums are defined in `@shared/constants/` and must be used as the single
+source of truth for message types.
+
+## Adding a New Message Type
+
+### Step 1: Add to the Enum
+
+Add the new type to `SERVICE_TYPES` (for popup-to-background) or
+`EXTERNAL_SERVICE_TYPES` (for dApp-to-background):
+
+```typescript
+// @shared/constants/services.ts
+export enum SERVICE_TYPES {
+ // ... existing types
+ GET_NEW_DATA = "GET_NEW_DATA",
+}
+```
+
+### Step 2: Create a Handler
+
+Create a handler function in
+`extension/src/background/messageListener/handlers/`:
+
+```typescript
+// extension/src/background/messageListener/handlers/getNewData.ts
+export const handleGetNewData = async (request: MessageRequest) => {
+ try {
+ const data = await fetchNewData(request.params);
+ return { result: data };
+ } catch (error) {
+ captureException(error);
+ return { error: "Failed to fetch data" };
+ }
+};
+```
+
+### Step 3: Register in the Listener
+
+Register the handler in the background message listener:
+
+```typescript
+case SERVICE_TYPES.GET_NEW_DATA:
+ return handleGetNewData(request);
+```
+
+### Step 4: Send from the Popup
+
+Use `sendMessageToBackground()` from `@shared/api/internal`:
+
+```typescript
+import { sendMessageToBackground } from "@shared/api/internal";
+import { SERVICE_TYPES } from "@shared/constants/services";
+
+const response = await sendMessageToBackground({
+ type: SERVICE_TYPES.GET_NEW_DATA,
+ params: {
+ /* ... */
+ },
+});
+```
+
+## Response Queue Pattern
+
+For operations requiring user approval (signing, connecting):
+
+1. Background generates a unique ID via `crypto.randomUUID()`
+2. A promise resolver is stored in a response queue array, keyed by the UUID
+3. Background opens the approval window with the UUID as a URL parameter
+4. When the user responds, the approval window sends the result back
+5. The matching resolver is found by UUID, called with the result, and spliced
+ from the array
+6. Timeout cleanup removes stale entries if the user closes the window without
+ responding
+
+## Response Structure (CRITICAL)
+
+Handlers MUST return structured objects rather than throwing. Follow this exact
+pattern:
+
+### Success Response
+
+Return domain-specific fields WITHOUT an `error` field:
+
+```typescript
+// CORRECT success responses
+return { signedTransaction: "XDR..." };
+return { publicKey: "G...", allAccounts: [...] };
+return { preparedTransaction: xdr, simulationResponse };
+```
+
+### Error Response
+
+Return ONLY `{ error: string }` — no domain-specific fields mixed in:
+
+```typescript
+// CORRECT error response
+return { error: "Soroban simulation failed" };
+
+// WRONG — mixing error with domain fields
+return { error: "Soroban simulation failed", simulationResponse }; // DON'T DO THIS
+```
+
+### Why This Matters
+
+The SDK and popup code check for the presence of `error` to determine
+success/failure. Mixing error with data fields creates ambiguous responses.
+
+### Exception: apiError for SDK consumers
+
+When the caller is `@stellar/freighter-api`, include `apiError` alongside
+`error` for detailed diagnostics:
+
+```typescript
+return {
+ error: "User declined",
+ apiError: "User rejected the transaction request",
+};
+```
+
+## Shared API Layer
+
+The `sendMessageToBackground()` function is the only approved way to send
+messages from the popup to the background. It is defined in
+`@shared/api/helpers/extensionMessaging.ts` and re-exported from
+`@shared/api/internal.ts`:
+
+- Wraps `browser.runtime.sendMessage` with proper typing
+- Handles response parsing and error extraction
+- Never call `browser.runtime.sendMessage` directly from popup code
diff --git a/docs/skills/freighter-best-practices/references/performance.md b/docs/skills/freighter-best-practices/references/performance.md
new file mode 100644
index 0000000000..2677759f8f
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/performance.md
@@ -0,0 +1,241 @@
+# Performance -- Freighter Extension
+
+Based on analysis of 224 component files. Current performance score: **5.9/10**.
+
+## CRITICAL: Complete Implementations First
+
+**Performance patterns are optimizations WITHIN complete implementations, not
+replacements.**
+
+When asked to create a component that needs derived state:
+
+1. **First** produce the complete React component with UI, props interface, and
+ rendering logic
+2. **Then** apply performance optimizations (createSelector, useMemo,
+ useCallback, React.memo)
+
+```typescript
+// WRONG — only selectors, no component
+export const selectTotalBalance = createSelector([...], ...);
+// Missing: the actual component that USES these selectors
+
+// CORRECT — complete implementation with performance optimizations
+// 1. Memoized selectors (performance layer)
+const selectTotalBalance = createSelector([balancesSelector], (balances) => ...);
+
+// 2. Props interface (type safety)
+interface DashboardProps {
+ title: string;
+}
+
+// 3. Complete component (UI layer)
+export const Dashboard = ({ title }: DashboardProps) => {
+ const totalBalance = useSelector(selectTotalBalance);
+ const formattedBalance = useMemo(() => formatBalance(totalBalance), [totalBalance]);
+
+ return (
+
+
{title}
+ {formattedBalance}
+
+ );
+};
+```
+
+**Never produce only selectors when a component is requested.**
+
+## React.memo -- CRITICAL GAP
+
+Only **1 memo() usage** in the entire popup codebase (`AccountAssets/AssetIcon`
+with custom `shouldAssetIconSkipUpdate` comparator). This is a critical
+performance gap.
+
+**RULE: Components rendered in lists or receiving frequently-changing parent
+props MUST use `React.memo()`.**
+
+Components that should be memoized:
+
+- All list item components (TokenList items, AssetRows, OperationsKeyVal)
+- AccountTabs, AccountHeader sub-components
+- ManageAssetRows children
+
+```typescript
+// REQUIRED for list-rendered components
+export const AssetRow = memo(
+ ({ asset, onSelect }: AssetRowProps) => { ... },
+ (prev, next) => isEqual(prev.asset, next.asset),
+);
+```
+
+## useMemo -- 13 occurrences across 7 files (GOOD)
+
+Currently used for:
+
+- Conditional computations (muxed checks, contract validation)
+- Debounced function creation
+- Object creation in dependencies
+
+**RULE: Wrap in useMemo when:**
+
+1. Computing derived values from expensive operations (parsing, formatting,
+ asset lookups)
+2. Creating objects/arrays that are passed as props to memoized children
+3. Values used as useEffect dependencies that would otherwise change every
+ render
+
+```typescript
+// REQUIRED — computed value passed as prop
+const formattedBalances = useMemo(
+ () => balances.map((b) => formatBalance(b, network)),
+ [balances, network],
+);
+
+// NOT NEEDED — simple primitive
+const isMainnet = network === NETWORKS.PUBLIC;
+```
+
+## useCallback -- 15 occurrences across 9 files (MODERATE)
+
+**RULE: ANY callback passed as a prop MUST be wrapped in useCallback.**
+
+```typescript
+// REQUIRED — passed to child component
+const handleSelect = useCallback(
+ (asset: Asset) => { dispatch(selectAsset(asset)); },
+ [dispatch],
+);
+
+// CRITICAL ANTI-PATTERN — 130 inline arrows found in codebase
+// WRONG:
+ handleClick(asset)} />
+
+// CORRECT:
+const onClick = useCallback(() => handleClick(asset), [asset]);
+
+```
+
+**High-priority files needing useCallback refactoring:**
+
+- `AccountAssets/index.tsx` — 10+ inline handlers
+- `ManageAssetRows/ChangeTrustInternal/index.tsx` — 10 inline handlers
+- `AccountHeader/index.tsx` — 10+ inline handlers
+
+## Inline Functions in JSX -- CRITICAL (130 occurrences)
+
+**130 inline arrow functions** across 54 files. **15 inline style objects**
+across 8 files.
+
+**RULE: Never create inline functions inside .map() or list renders. Extract to
+useCallback.**
+
+```typescript
+// WRONG — creates new function per render per item
+{balances.map((b) => (
+ handleClick(b.canonical)} key={b.canonical} />
+))}
+
+// CORRECT
+const handleRowClick = useCallback((canonical: string) => {
+ handleClick(canonical);
+}, [handleClick]);
+
+{balances.map((b) => (
+
+))}
+```
+
+## Selector Memoization -- createSelector in 4 files (GOOD)
+
+`createSelector` from reselect is used in `remoteConfig.ts`, `settings.ts`,
+`cache.ts`, `accountServices.ts`. 161 `useSelector` calls across 81 files.
+
+**RULE: All derived/computed state MUST use `createSelector`. Never compute
+inline in components.**
+
+```typescript
+// CORRECT — memoized selector
+export const selectFormattedBalances = createSelector(
+ [selectBalances, selectNetwork],
+ (balances, network) => balances.map((b) => formatBalance(b, network)),
+);
+
+// WRONG — recomputes every render
+const formatted = balances.map((b) => formatBalance(b, network));
+```
+
+## useEffect Dependencies -- 48 eslint-disable-next-line
+
+48 `react-hooks/exhaustive-deps` suppressions across the codebase.
+
+**RULE: Every eslint-disable for exhaustive-deps MUST have a comment explaining
+WHY.**
+
+```typescript
+// ACCEPTABLE — documented intentional mount-only effect
+useEffect(() => {
+ fetchInitialData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps — intentional mount-only fetch
+}, []);
+
+// WRONG — bare suppression
+useEffect(() => {
+ fetchData(publicKey, network);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+}, [publicKey]);
+```
+
+## Code Splitting -- 0 React.lazy (MISSED OPPORTUNITY)
+
+No `React.lazy` or `Suspense` usage.
+
+**RULE: Lazy-load components not needed on initial render:**
+
+- Debug views (`Debug.tsx`, `IntegrationTest.tsx`)
+- Heavy modal/drawer components
+- Detail views that load on navigation
+
+```typescript
+const Debug = React.lazy(() => import("popup/views/Debug"));
+
+// In router:
+}>
+ } />
+
+```
+
+## Context Usage -- EXCELLENT (3 contexts only)
+
+Only 3 React contexts exist (`InputWidthContext`, `AccountTabsContext`, `View`).
+All hold simple primitive values. Do not add more contexts for data state — use
+Redux.
+
+## Key Props -- 4 index-as-key anti-patterns
+
+4 files use array index as key. Fix these:
+
+- `GrantAccess/index.tsx`
+- `SignMessage/index.tsx`
+- `TransactionDetail/index.tsx`
+- `ConfirmMnemonicPhrase/index.tsx`
+
+**RULE: Use stable unique identifiers as keys, not array indices.**
+
+## useReducer + Redux Dispatch Separation
+
+For complex async flows, separate local UI loading state from global Redux
+state:
+
+- Use `useReducer` for local component state (loading spinners, form validation,
+ step tracking)
+- Use Redux dispatch for global state changes (account data, network state,
+ transaction results)
+
+## Performance Priority Actions
+
+| Priority | Action | Impact |
+| -------- | ------------------------------------------------- | ---------------------------- |
+| **P0** | Add React.memo() to list item components | 30-40% fewer re-renders |
+| **P0** | Convert 130 inline arrow functions to useCallback | Stabilize reference equality |
+| **P1** | Add React.lazy for debug/modal views | Reduce initial bundle |
+| **P1** | Document all 48 exhaustive-deps suppressions | Prevent future bugs |
+| **P2** | Add useMemo for computed values in map() renders | Prevent recomputation |
diff --git a/docs/skills/freighter-best-practices/references/security.md b/docs/skills/freighter-best-practices/references/security.md
new file mode 100644
index 0000000000..69155ff0cd
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/security.md
@@ -0,0 +1,112 @@
+# Security -- Freighter Extension
+
+## Key Storage Architecture
+
+Freighter is a non-custodial wallet. Private keys never leave the device.
+
+- **Encrypted vault:** stored in `chrome.storage.local`, encrypted with the
+ user's password
+- **Decrypted keys:** held only in `chrome.storage.session` (memory-only,
+ cleared when the browser session ends)
+- **Keys exist only in the background context** -- the popup and content scripts
+ never have access to raw key material
+
+## Popup Never Sees Raw Keys
+
+The popup sends unsigned XDR to the background and receives signed XDR back. The
+signing flow:
+
+1. Popup builds an unsigned transaction (XDR)
+2. Popup sends XDR to background via `sendMessageToBackground()`
+3. Background decrypts the key from session storage, signs the XDR
+4. Background returns the signed XDR to the popup
+5. Popup submits the signed transaction to the network
+
+## Hardware Wallet Path
+
+Hardware wallets (Ledger) use the same interface as software signing. The
+background detects the key type and routes to the appropriate signing backend
+(local key vs. hardware device). The popup code is identical for both paths.
+
+## Message Validation
+
+Every message received by the background must pass validation. The required
+checks depend on data sensitivity:
+
+1. **Valid message type** -- the message type must be a member of the
+ `SERVICE_TYPES` or `EXTERNAL_SERVICE_TYPES` enum (all messages)
+2. **Sender origin check** -- verify the message comes from a trusted source
+ (sensitive endpoints only)
+3. **Allow list check** -- the dApp's origin must be in the user's approved
+ list, or trigger an approval prompt (sensitive endpoints only)
+4. **Public key existence** -- the requested account must exist in the wallet
+ (user-specific endpoints only)
+
+### Allowlist Rules by Endpoint Type
+
+| Endpoint Type | Allowlist Check | Examples |
+| --------------------------------------------- | --------------- | -------------------------------------------------------------------- |
+| Read-only non-sensitive (network info) | NOT required | `requestNetwork`, `requestNetworkDetails`, `requestConnectionStatus` |
+| User-specific sensitive (public key, signing) | Required | `requestPublicKey`, `requestAccess`, `signTransaction` |
+| State-modifying | Required | `setAllowedStatus` |
+
+## Window-Based Approval Flow
+
+When a dApp requests a sensitive operation (signing, account access):
+
+1. Background generates a unique request ID via `crypto.randomUUID()`
+2. Background opens a new browser window with the approval UI, passing params
+ via URL encoding
+3. A promise resolver is stored in a response queue, keyed by the request ID
+4. When the user approves or rejects, the approval window sends the response
+ back
+5. The promise resolves with the result, and the entry is spliced from the
+ response queue
+
+## Content Script Filtering
+
+The content script acts as a gatekeeper between the page and the extension:
+
+- Listens for `window.postMessage` events from the page
+- Filters by source: only messages with `EXTERNAL_MSG_REQUEST` source are
+ processed
+- Validates the message type against `EXTERNAL_SERVICE_TYPES` enum
+- Only valid messages are forwarded to the background via
+ `chrome.runtime.sendMessage`
+- All other messages are silently dropped
+
+## Content Security Policy
+
+Manifest V3 enforces strict CSP by default:
+
+- No inline scripts (`script-src 'self'`)
+- No `eval()` or dynamic code generation
+- Minimal permissions: only `storage` and `alarms` are declared in the manifest
+
+## Blockaid Integration
+
+- Every dApp transaction is scanned by Blockaid before signing
+- Blockaid provides risk assessment (malicious, benign, unknown)
+- Debug panel available at the `/debug` route in the extension popup during
+ development (requires dev mode)
+- Scan results are displayed to the user in the approval UI
+
+## Production Guardrails
+
+- Production builds block connections to the dev server
+- Additional security checks are enabled in production mode
+- Source maps are not shipped in production builds
+
+## Common Security Mistakes to Avoid
+
+- **Logging keys:** never log private keys, mnemonics, or decrypted key material
+ to the console
+- **Weakening CSP:** never add `unsafe-inline` or `unsafe-eval` to the manifest
+ CSP
+- **Trusting page content:** never use data from `window.postMessage` without
+ validation against the enum
+- **Non-null assertions on key material:** `privateKey!` can mask missing keys
+ -- always handle the undefined case explicitly
+- **Storing secrets in local storage:** `chrome.storage.local` persists across
+ sessions and is not memory-only. Use `chrome.storage.session` for decrypted
+ material
diff --git a/docs/skills/freighter-best-practices/references/testing.md b/docs/skills/freighter-best-practices/references/testing.md
new file mode 100644
index 0000000000..1b388e65d5
--- /dev/null
+++ b/docs/skills/freighter-best-practices/references/testing.md
@@ -0,0 +1,91 @@
+# Testing -- Freighter Extension
+
+## Jest Unit Tests
+
+- Test files live in `__tests__/` directories alongside the source they test
+- Environment: JSDOM (`jest-fixed-jsdom`)
+- Coverage collected from `src/**/*.{ts,tsx,mjs}`
+- Configuration in the root `jest.config.js`
+
+### Mocking Pattern
+
+Use typed mocks via `jest.requireMock` for type-safe mock access:
+
+```typescript
+const { someFunction } = jest.requireMock<
+ typeof import("@shared/api/internal")
+>("@shared/api/internal");
+```
+
+Factory functions create consistent test fixtures:
+
+- `makeStore()` -- creates a Redux store with test-specific initial state
+- `store.getState()` -- standard Redux method to get a snapshot of the current
+ store state
+
+### Testing Redux
+
+1. Create a test store with `configureStore()` and the real reducers
+2. Dispatch thunks against the test store
+3. Assert state changes via `store.getState()`
+
+```typescript
+const store = makeStore({
+ preloadedState: {
+ /* ... */
+ },
+});
+await store.dispatch(fetchAccountBalances("GABC..."));
+expect(store.getState().accountBalances.status).toBe(ActionStatus.SUCCESS);
+```
+
+### Testing Background Handlers
+
+Call handler functions directly with mock parameters:
+
+```typescript
+const response = await handleSignTransaction({
+ transactionXdr: "AAAA...",
+ publicKey: "GABC...",
+});
+expect(response).toEqual({ signedTransaction: "signed-xdr" });
+expect(captureException).not.toHaveBeenCalled();
+```
+
+## Playwright End-to-End Tests
+
+- **Browser:** Chromium only
+- **Viewport:** 1280x720
+- **Timeout:** 15 seconds per test
+- **Retries:** 5
+- **Workers:** 8 locally, 4 in CI
+
+### Fixtures
+
+The `test-fixtures.ts` file provides:
+
+- A browser context with the extension loaded
+- Extension ID extraction from the service worker URL
+- Direct access to the service worker for background testing
+
+### Snapshot Testing
+
+- Snapshots stored in `[testName].test.ts-snapshots/`
+- Update snapshots with `--update-snapshots` flag
+
+## Running Tests
+
+| Command | Description |
+| ------------------------- | ------------------------------------ |
+| `yarn test` | Run Jest in watch mode |
+| `yarn test:ci` | Run Jest for CI (no watch, coverage) |
+| `yarn test:e2e` | Run all Playwright e2e tests |
+| `yarn test:e2e --headed` | Run e2e with visible browser |
+| `yarn test:e2e --ui` | Run e2e with Playwright UI mode |
+| `PWDEBUG=1 yarn test:e2e` | Run e2e with Playwright debugger |
+
+## CI Pipeline
+
+- **`runTests.yml`** -- runs Jest unit tests and Playwright e2e tests on every
+ pull request
+- **`codeql.yml`** -- runs CodeQL security analysis on every pull request