This file is the project guide for every coding agent: Claude Code, Codex,
Cursor, Hermes, GitHub agents and others. It applies to the whole repository.
An AGENTS.md file in a subdirectory adds rules for the files under it.
Keep all project guidance in this file. The repository has no CLAUDE.md
on purpose. Claude Code 2.1.277 and later reads AGENTS.md only when no
CLAUDE.md exists, so a root CLAUDE.md hides this file from Claude Code.
Put guidance here also when a request says "add this to CLAUDE.md". If a tool
creates a root CLAUDE.md, move its useful content into this file and delete
CLAUDE.md. Claude-specific configuration lives in .claude/ (skills, agents,
hooks, settings.json).
-
Write all new code in TypeScript strict mode, unless the request says otherwise.
-
Run
yarncommands from the repository root. Do not runyarn buildinside a workspace directory, because it writes an incorrect output structure. -
Common commands:
yarn install && yarn start # Dev mode yarn build # Rollup compile to app/ yarn lint && yarn test # Lint + test yarn workspaces:build # Build all workspaces
-
After you build
desktop-release-action, delete the nested dist withrm -rf workspaces/desktop-release-action/dist/dist. The action needs onlyworkspaces/desktop-release-action/dist/index.js. -
app/mirrorssrc/publicthrough thesyncPublicAssets()rollup plugin. A build purges fromapp/each file that you delete fromsrc/public. Theyarn startwatcher rebuilds and relaunches the app when asrc/publicasset changes.
devis the default branch. ALL feature and fix PRs targetdev, and they are squash-merged.masterholds only released code, and its tip is always the latest stable release. It advances through adev→masterrelease PR, and through arelease/X.Y.x→masterPR after each patch of the newest stable minor. Merge both with a true merge commit (gh pr merge --merge). NEVER squash them. A squash forks the history permanently.- A
release/X.Y.xbranch is the patch line for a shipped stable version. A fix lands ondevfirst, and then is cherry-picked onto the release branch. When you write a hotfix directly on a release branch, forward-port it todevimmediately with a cherry-pick PR. - Never back-merge
masteror arelease/X.Y.xbranch intodev. When thedev→masterrelease PR conflicts (the patch version bump onmaster), never resolve it in the GitHub UI. Use a promotion branch, asdocs/release-process.mddescribes. - Version invariant:
package.jsonondevalways equals the newest tag cut from thedevline. The first alpha of a new cycle bumps straight toX.(Y+1).0-alpha.1. - Details:
docs/development-and-release-flow.md(concepts) anddocs/release-process.md(exact commands).
- The repository uses two patch systems. Put each patch in the system that
already owns its package:
- Yarn patch protocol:
.yarn/patches/, configured inpackage.json. Today it patches@ewsjs/xhrandapp-builder-lib(the macOS keychain fix from #3499). patch-package:patches/. Today it patches@kayahr/jest-electron-runner.
- Yarn patch protocol:
- Never add an
@ewsjs/xhrpatch topatches/. That causes CI conflicts. - The dev and snapshot code paths of
desktop-release-action(releaseDevelopment/releaseSnapshot) are dead on purpose. Do not rebuild itsdist/bundle only to delete them. - Windows builds must include all three architectures:
x64,ia32andarm64. - Code signing uses Google Cloud KMS in two phases. This prevents MSI build
failures from KMS CNG provider conflicts.
- Build the packages without signing (empty env vars).
- Sign the built packages with
jsignand Google Cloud KMS.
electron-builder.json'smac.bundleVersion(macOSCFBundleVersion) is independent from theversioninpackage.json. Bump it on every release that ships to the App Store or gets notarized. Apple requires each submission'sCFBundleVersionto be higher than the last one.- Format:
YYMMplus a single-digit build counter that resets to0each month. Example: the first build shipped in August 2026 is26080, and the second build in the same month is26081. - Before you increment it, read the current value and the date of its last
bump (
git log -p --follow -- electron-builder.json). Do not guess.
- Format:
yarn build-assetsre-encodes every PNG and ICO that it touches, also when the source did not change. Commit only the assets whose SVG or component changed, andgit checkout --the rest. Otherwise byte noise floods the diff.
- Create release tags only with
yarn release:tag. Do not tag with a manualgit tagandgit push. The script (scripts/release-tag.ts) checks the version, the channel and the allowed remote refs, and it fails closed. A manual tag skips all of those checks. - Tag the squashed merge commit or bump commit on the branch that the channel
expects. That is
devfor a prerelease, andmasteror arelease/X.Y.xbranch for a stable release. Never tag a pre-merge bump commit on an unmerged branch, because that ships the wrong tree. - When a guard fires, treat it as a real finding: report it and fix the cause.
Use
--forceor--allow-unverified-refonly when the release owner explicitly agrees. - A fresh worktree has no
node_modules, and the script fails withCouldn't find the node_modules state file (findPackageLocation). Runyarn installin the worktree. Never tag by hand to work around it. - A tag name is the bare version (
4.16.0, novprefix). It MUST equal theversioninpackage.json, because the auto-updater feed derives from that version. - A tag push starts
build-release.yml, which always creates a draft release for a human to review and publish. The workflow has apreparejob (creates the draft) and seven parallel packaging jobs.- Only ONE packaging job per platform may set
upload_update_metadata: 'true'(nsis, dmg, AppImage). Thelatest*.ymlthat a job uploads lists only the files that the job built. A second uploader replaces the file with a partial list and breaks auto-update. workflow_dispatchon that workflow is a signed dry run that creates no release. Run it before you trust a change to the workflow or the action with a real tag. The action reads itsmode,targetsandupload_update_metadatainputs fromworkspaces/desktop-release-action/action.yml.
- Only ONE packaging job per platform may set
- Full reference (guard table, overrides, channel rules, flags):
docs/release-process.md. Theship-releaseskill drives the end-to-end flow: notes, bump PR, merge, tag, CI, asset matrix and Jira release sync.
- Use Fuselage components from
@rocket.chat/fuselagefor UI work, unless the design needs something that Fuselage does not provide. - Check
Theme.d.tsfor valid color tokens before you use a Fuselage color. - For usage patterns, see the Fuselage Storybook and the Rocket.Chat main repo.
- Check library props, APIs and tokens in the official docs or the local
.d.tsfiles. Do not assume them. - Read
docs/desktop-ui-guidelines.mdbefore you style tab bar or titlebar controls, draw custom SVG artwork, or pick color or animation tokens. It holds the token semantics and traps, and the Fuselage geometry and timing facts. It also holds the button-dimming and SVG transform-origin pitfalls, and the layout rules from PRs #3441/#3443. - Tray icons show status only, by default, on macOS, Windows and Linux. Each
platform has six states:
default,presence-{online,away,busy,offline}anddisconnected(src/ui/main/icons.ts).- The unread count shows on the Windows taskbar overlay (
rootWindow.tssetOverlayIcon), in the macOS menu-bar title and in the Linux tray tooltip. - The opt-in
isTrayIconUnreadCounterEnabledsetting (#3484) replaces presence with the pre-4.17 badge assets:notification-{dot,1..9,plus-9}, andnotificationTemplateon macOS. Regenerate them withyarn build-assets --unread-counter. The disconnected state still wins. - The macOS-only
isMenuBarUnreadCountEnabledsetting (CORE-2703, default on) hides the number in the menu-bar title. It does not change the Dock badge.
- The unread count shows on the Windows taskbar overlay (
- Presence bullets reuse the Fuselage
StatusBulletglyphs:src/ui/icons/PresenceBullet.tsx(filled / clock cut-out / bar cut-out / hollow ring) andDisconnectedBadge.tsx(filled amber with a bold!). Any overlay with transparent cut-outs or a hollow shape MUST pass thecutoutprop ofAppIcon(PresenceBulletCutout). Otherwise the rocket shows through the holes. - Tray-menu bullet icons are 12pt assets under
src/public/images/presence/. They match the 12px Fuselage bullet next to 14px text. Regenerate them withyarn build-assets --presence-menu-icons.
-
Spec names and locations decide which Jest project runs a spec (
testMatchinjest.config.js):- Main-process specs live under
src/<module>/main/(named*.main.spec.tsby convention), or they are namedsrc/<module>/main.spec.ts. Themain/directory selects the main-process runner, not the.main.spec.tssuffix. - Renderer specs (
*.spec.ts/*.spec.tsx) live in a nested path such assrc/<module>/<subdir>/*.spec.ts(x), or they are namedsrc/<module>/renderer.spec.ts(x). Jest does not discover any other flatsrc/<module>/*.spec.tsfile. - Specs under
scripts/run in their own Jest project, withtestEnvironment: 'node'. - When you are not sure that Jest finds a new spec, run
yarn test --listTests --runTestsByPath <file>. - Run specs with
yarn test <path>. The script adds--forceExitandxvfb-maybe. A barenpx jesthas no--forceExit, and it has hung after the suite passed.
- Main-process specs live under
-
Tests run on Windows, macOS and Linux CI. Make each change and each spec work on all three platforms.
-
For platform-specific APIs, prefer optional chaining and fallbacks to mocks:
// PREFERRED — works on all platforms without mocks const uid = process.getuid?.() ?? 1000; const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid?.() ?? 1000}`;
Mock only when defensive code is not possible. These POSIX-only APIs (undefined on Windows) need it:
process.getuid(),process.getgid(),process.geteuid()andprocess.getegid(). -
UI changes need runtime or visual verification, because component tests cannot see paint. A clipped SVG passes every DOM assertion.
- Use the
dev-app-verifyskill (skills/dev-app-verify/SKILL.md) to drive and screenshot the runningyarn startapp through the port-9339 inspector. - Use the Developer Mode menu items
Simulate Update Flow,Simulate DownloadandSimulate Disconnectedto run those flows without real downloads or updates. Simulate Disconnectedis a Developer menu checkbox. It forces the presence connection of the active workspace todisconnectedon the read side only, and the real connection stays up. Use it to check the disconnected tray icon and menu line without a network drop.
- Use the
-
You CANNOT test screen capture, WebRTC or portal behavior in a software-rendered VM. Chromium gates the PipeWire capture path on hardware GL, and the gate moves between Electron versions. Test on hardware GL (GPU passthrough or a physical machine). Assert at the dbus level (
org.freedesktop.portal.ScreenCastrequests), not on dialog visibility, which is flaky across portal and boot states. Full story:docs/postmortem-screen-picker-startup-enumeration.md. -
The desktop app reads the web client's
windowevents (startup,unread-changed*,status-changed,Custom_Script_Logged_InandCustom_Script_Logged_Out). Before you change code that reads them, runyarn probe:web-signals --server <url>against a disposable server. The probe checks the Meteor build and the Vite build. Follow theweb-signals-probeskill (skills/web-signals-probe/SKILL.md). Do not readCustom_Script_On_Logout, because the Meteor build does not fire it on a forced logout.
@kayahr/jest-electron-runnersimulates the Electron environment. It forces--maxWorkers=1and spawns one Electron process per spec file. The only CI parallelism is the cross-job--shard, and since the transform fix that spawn is most of each shard's ~3 min. Do NOT try more workers, other runner classes or a faster transformer to cut that floor.- ts-jest runs transpile-only (
tsconfig: { isolatedModules: true }inline injest.config.js, withtsconfig.jsonunchanged). Do NOT switch back topreset: 'ts-jest'. That default builds a type-checking Program per spec file, CI has no warm Jest cache, and the Test step became 4× slower.tsc --noEmitinyarn lintcatches type errors in specs. - Do NOT switch the transformer to
@swc/jestbefore you fix the two spec patterns that it breaks. The measured gain is ~20 s per shard. Story:docs/postmortem-validate-pr-ci-speed.md.- An assignment onto
require(mod).fnfails, because swc emits read-only getters. - A read of a
constinside ajest.mockfactory fails, because swc's hoisting hits the temporal dead zone.
- An assignment onto
- A spec with the
@jest-environment jsdomdocblock runs in its own Jest project, in plain Node.jest.config.jsfinds these specs by the docblock. Do not run jsdom inside the Electron runner. Since Electron 43, the renderer aborts withCheck failed: ToExecutionContext(context), and the runner hangs until CI kills the job. These specs also count under--coverage. - validate-pr runs the ubuntu shards on
ubuntu-24.04-arm.- Cache keys MUST include
runner.arch, because both architectures reportrunner.os == Linux. - The test job sets
PUPPETEER_SKIP_DOWNLOAD, because the puppeteer postinstall has no arm64 Linux Chromium. Onlyyarn build-assetsuses puppeteer.
- Cache keys MUST include
- Before you blame the CI runners for a slowdown, compare the per-step
timings (
gh run view <id> --json jobs) across months. In Sep 2026 the Windows Test step had grown from 1 min to 27 min only from suite growth and per-file type-checking. The runners did not change.
- A Windows Action Center card stays repliable with no time limit. Electron
emits the JS
closeevent when the banner times out (NotificationDismissed(should_destroy=false)), and a call toclose()on the instance does NOT remove the card.- Keep per-notification state (reply routing, target webContents, preload event handlers) until the app decides that the notification is finished. Keep that state bounded.
- Do NOT tie that state to a
closeor dismiss signal. If you do, the app drops replies that the user types later.
- The web client closes every desktop notification 10s after it shows it
(
useNotification.ts). The server never sendsduration, so the client always uses that fallback. Expect that close while the card is still on screen. - Only reply and action-button interactions carry activation arguments
(
type=...&tag=<id>, read throughNotification.handleActivation, win32). A click on the toast body carries none and arrives ONLY as the instanceclickevent. Keep that listener unconditional. Notification.handleActivationREPLACES the stored callback. It does not add one. A debug probe that registers its own handler silently unregisters the app's handler. Do not use such a probe to observe production behavior.- Enter does NOT submit a toast reply. Only the Reply button of the toast does, and the card closes on submit whether or not the app received the reply. To check a reply, query the server for the message. Do not trust the UI.
- Log dropped activations through
loggers.notifications(src/logging/scopes.ts). Packaged builds do not showconsole.warnfrom the main process, so a dropped reply would leave no trace in the field. - Full investigation history:
docs/postmortem-notification-quick-reply-sup-1097.md.
- For the first ~60 s,
yarn startrelaunches Electron once per rollup bundle (eachwriteBundlerestarts the app). Untilwaiting for changesprints, the tray or menu-bar icon flickers or is absent. Judge the tray state only after that line. - Before you judge the tray,
pkillorphaned worktree Electrons. A leftover instance keeps port 9339 and shows a second menu-bar icon. WEBVIEW_SERVER_VERSION_UPDATEDis the webview boot-success signal.setVersiondispatches it fromsetServerInfo.- Key a boot cycle on a committed navigation (
did-navigate). Do NOT key it ondid-start-loading. That event fires for subframes and resources, and it causes false boot-failure reports. - When a workspace stays on the loading throbber (the "boot wedge"), follow
.claude/skills/boot-wedge-debug/SKILL.md(watchdog report, live CDP autopsy, cure ladder).- If the boot watchdog was enabled for the run (dev mode or
ROCKETCHAT_BOOT_WATCHDOG=true), readboot-watchdog.jsonlfirst. It is in Electron'sapp.getPath('logs')directory (~/Library/Logs/Rocket.Chat/on macOS). - If the watchdog was disabled, it wrote no report. Start with the live CDP autopsy.
- Full investigation history:
docs/postmortem-webview-boot-wedge.md.
- If the boot watchdog was enabled for the run (dev mode or
- A startup
Maximum update depth exceededstorm depends on data (unread rooms at boot). Before you blame recent code for an intermittent startup failure, bisect against a clean baseline build. The storm reproduced on the untouched 4.16.0-alpha.2 baseline (PR #3437).
Before you create or change a QA asset under qa/, read qa/AGENTS.md. It
holds the authoring rules, the template and the validation commands. For a QA
pass on a Desktop PR, branch or release candidate, follow
skills/desktop-qa-flows/SKILL.md.
- TypeScript strict mode.
- React functional components with hooks.
- Redux actions follow the FSA (Flux Standard Action) shape.
- File names: camelCase for files, PascalCase for components.
- Write self-documenting code with clear names. Add a comment only when the code cannot say why.
- Keep every name that a preload exposes to the web client
(
RocketChatDesktop,videoCallWindowand their methods) backward compatible. A rename adds the new name and keeps the old name as an alias of the same object. Remove an old name only when no supported server version reads it. Users do not update the desktop app and the server together. RemovingvideoCallWindowin 4.18.0 broke leaving a call on Rocket.Chat 8.9.0 (DMV2-32). - Prefer an edit to an existing file over a new abstraction. Add an abstraction only when it removes real complexity or matches an existing pattern.
- Commit or push only when the user explicitly asks for it. "Fix this" does NOT mean "commit it".
- Never commit directly to
master,devorrelease/X.Y.x. Create a branch, test, and open a PR. - Read-only git operations (status, diff, log) need no permission.
- Show what you will commit before you commit.
Use a worktree, so that you do not disturb another working directory. Start
it from the current remote dev, because a local dev can be stale:
mkdir -p ../Rocket.Chat.Electron-worktrees
git fetch origin dev
git worktree add ../Rocket.Chat.Electron-worktrees/feature-name -b new-branch origin/dev- Understand before you change. Find out WHY the code is written that way. Working code is correct until proven otherwise. If you are not sure, ASK.
- Verify your work. Run the tests and the type check (
npx tsc --noEmit), and show that the change works. Start with the narrowest meaningful check. Broaden the checks when the risk or the shared behavior justifies it. Never mark a task done before you prove that it works. - Diagnose before you iterate. When an approach fails, find WHY before you try the next one. Do not cycle blindly through 3 or more approaches.
- Check libraries. Read the official docs and the
.d.tsfiles innode_modules/. Never assume that a prop, token or API works.
- Do not use subjective descriptors ("smart", "excellent", "dumb").
- Use measurable descriptions: "reduced memory usage", "improved by X%".
- Never invent metrics: no estimated time spent, no speculated user counts. Use only numbers from real logs, error messages or documented sources.
- PR descriptions: use straightforward language, and focus on what changed and why.
- PR descriptions and commit bodies are public. Do not include internal references: Jira or Zoho Desk links and ticket text, customer names, support conversations. Do not include notes about local developer tools either (GitNexus, agent workflow, ledgers). A bare Jira key in the title or subject is the accepted convention. Everything else about the ticket stays internal.
- Write instructions for agents (this file, nested
AGENTS.mdfiles, skills) in short active sentences: one instruction per sentence, and no semicolons.
This project is indexed by GitNexus as Rocket.Chat.Electron. Use the
GitNexus MCP tools to understand code, assess impact and navigate safely. Read
gitnexus://repo/Rocket.Chat.Electron/context for the current index stats and
staleness. If GitNexus is not available, do not block on it. Use local code
search, tests and careful review instead.
- Reindex only from the main checkout, at a quiet point, with
node .gitnexus/run.cjs analyze --index-only. No.gitnexus/run.cjsyet? Runnpx gitnexus analyze --index-only(if npm 11 crashes, runnpm i -g gitnexus, see #1939).- Do not run
analyzeinside a linked worktree. It registers a second index under the same name,Rocket.Chat.Electron. Lookups by that name then fail with "Multiple registered repos match", and the extra index goes stale. To remove one, rungitnexus clean --forcein that worktree. --index-onlyskips every generated file. Without it,analyzerewrites the generated skills in.claude/skills/gitnexus/..gitnexusrcsetsskipAgentsMd, so thatanalyzedoes not rewrite this block and does not create a rootCLAUDE.md. Keep that setting. A GitNexus version that ignores.gitnexusrccreatesCLAUDE.md. Delete it if that happens.- Keep the quiet point. A background
analyzewas suspected of dropping freshly staged files and of restarting a runningyarn start. Upstream could not reproduce it (GitNexus #2906).
- Do not run
The MCP server serves the index of the main checkout, and it diffs the checkout that the session was started from. This stays true when you edit files in a linked worktree.
- Pass
repo: "Rocket.Chat.Electron"to every repo-scoped tool. Do not pass it tolist_repos, which takes onlylimitandoffset. A session started inside a worktree can fail without it. - Pass
worktree: "<absolute worktree path>"todetect_changes. Without it, a session started in the main checkout diffs the main checkout and misses every change in the worktree. query,contextandimpactdescribe the main checkout at its indexed commit.list_reposshows that commit and how far it is behind. For a symbol that your branch adds or changes, read the source.detect_changesmaps changed lines to symbols through that same index. A stale index can name the wrong symbol, so reindex the main checkout before a review that depends on exact symbols.
- MUST run impact analysis before you edit any symbol. Before you change a
function, class or method, run
impact({target: "symbolName", direction: "upstream"}). Report the blast radius (direct callers, affected processes, risk level) to the user. - MUST run
detect_changes()before you commit, to check that your changes affect only the expected symbols and execution flows.- For a regression review against the branch point, pass the merge base:
run
git fetch origin dev && git merge-base origin/dev HEAD, thendetect_changes({scope: "compare", base_ref: "<merge-base sha>"}). comparerunsgit diff <base_ref>against the working tree. A stale localdev, or anorigin/devthat moved past the branch point, adds changes that are not yours to the result.
- For a regression review against the branch point, pass the merge base:
run
- MUST warn the user when impact analysis returns HIGH or CRITICAL risk, before you continue with the edits.
- To explore unfamiliar code, use
query({query: "concept"})to find execution flows, not grep. It returns results grouped by process and ranked by relevance. - For full context on one symbol (callers, callees, the execution flows that
it is part of), use
context({name: "symbolName"}).
- NEVER edit a function, class or method before you run
impacton it. - NEVER ignore a HIGH or CRITICAL risk warning from impact analysis.
- NEVER rename symbols with find-and-replace. Use
rename, which understands the call graph. - NEVER commit changes before you run
detect_changes()to check the affected scope.
| Resource | Use for |
|---|---|
gitnexus://repo/Rocket.Chat.Electron/context |
Codebase overview, check index freshness |
gitnexus://repo/Rocket.Chat.Electron/clusters |
All functional areas |
gitnexus://repo/Rocket.Chat.Electron/processes |
All execution flows |
gitnexus://repo/Rocket.Chat.Electron/process/{name} |
Step-by-step execution trace |
| Task | Read this skill file |
|---|---|
| Understand architecture / "How does X work?" | .claude/skills/gitnexus/gitnexus-exploring/SKILL.md |
| Blast radius / "What breaks if I change X?" | .claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md |
| Trace bugs / "Why is X failing?" | .claude/skills/gitnexus/gitnexus-debugging/SKILL.md |
| Rename / extract / split / refactor | .claude/skills/gitnexus/gitnexus-refactoring/SKILL.md |
| Tools, resources, schema reference | .claude/skills/gitnexus/gitnexus-guide/SKILL.md |
| Index, status, clean, wiki CLI commands | .claude/skills/gitnexus/gitnexus-cli/SKILL.md |