Updating Dependencies
SkillSecurityLets your agent update npm and yarn dependencies in specified package files, including routine bumps and security updates.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Updating Dependencies skill
About this capability
Use when updating npm/yarn dependencies in shared/package.json, protocol/, or rnmodules/react-native-kb/. Use for routine dep bumps, security updates, or keeping packages current.
What this skill tells your AI
The instructions your AI receives, as published by keybase/client in skill/update-dependencies/SKILL.md and read by ahel’s review.
The react-native cluster
react-native is the anchor; everything else in this cluster follows it.
react-nativeitself is always upgradable. Take the in-line bump (0.86.0 → 0.86.2) as routine. A version-line jump (0.86 → 0.87) is a major — opt-in, and it drags the whole cluster with it. The check script reports both, and filters out react-native's1000.0.0main-branch sentinel.react,react-dom,react-is,react-test-renderer— pinned to whateverreact-nativerequires, never checked against npm on their own. Whenreact-nativemoves, read its peer deps (npm view react-native@<new> peerDependencies) and sync these by hand.@react-native/babel-preset,@react-native/eslint-config,@react-native/metro-config— must stay onreact-native's version line (react-native 0.86.x → these stay 0.86.x), but minor bumps within it ARE fine (0.86.0 → 0.86.2) — take them on routine passes. The check script caps these toreact-native's line automatically, so bumping RN's line pulls them along.
Version terminology in this project (all packages, not just react-native): a version-line change like 0.86 → 0.87 is a major; the last number (0.86.0 → 0.86.1) is a minor.
Other version constraints
expo and expo-* packages can be updated, but update them all together in one pass since they are versioned in sync.
webpack-dev-server: do NOT go past 5.x. v6 deleted the SockJS client, but @pmmmwh/react-refresh-webpack-plugin (latest 0.6.2, peer range ^4.8.0 || 5.x) still hard-requires webpack-dev-server/client/clients/SockJSClient in sockets/WDSSocket.js — so wds6 breaks desktop hot mode at compile time (ModuleNotFoundError). Stay on the latest 5.x (currently 5.2.6). Only move to wds6 once @pmmmwh ships a release whose peer range includes 6.x, or after migrating the bundler off webpack (see Rspack notes elsewhere). See memory [[project_wds6_react_refresh_sockjs]].
typescript is intentionally split into two packages — do NOT collapse them yet. TypeScript 7.x is the native (Go) rewrite: fast, but it does NOT ship the classic JS compiler API (ts.isCallExpression, ts.forEachChild, ts.Extension.*, etc.). Two consumers here still require that classic API: typescript-eslint (its @typescript-eslint/typescript-estree crashes at module-load on TS7 with Cannot read properties of undefined (reading 'Cjs'), and its peer range is >=4.8.4 <6.1.0 even on canary), and scripts/analyze-styles.mts (imports typescript and walks the AST). So:
"typescript": "6.0.3"— the classic-API package. Bareimport 'typescript'resolves here (eslint parser + analyze-styles). Keep on the latest6.x; do NOT bump to7.x."typescript-native": "npm:typescript@7.0.2"— TS7 native, aliased so it doesn't take the baretypescriptname. Used ONLY as thetscCLI, invoked by explicit path in thetscscript (./node_modules/typescript-native/bin/tsc). This replaced the old@typescript/native-preview(tsgo) dependency. Bump this to the latest stabletypescript@7.xon each pass.
When typescript-eslint ships a release that accepts TS7 (peer range includes >=7, and typescript-estree no longer reads the classic API off the typescript module): bump typescript-eslint, set "typescript": "<that 7.x>", delete the typescript-native alias, and repoint the tsc script back to ./node_modules/.bin/tsc. Verify analyze-styles.mts still type-checks (it may need porting to the TS7 API). Until then the split stays.
Process
1. Check what's outdated
Run the following script from shared/ — it checks all packages and derives pre-release vs stable from each package's current version automatically (no manual list to maintain):
cd shared && python3 ../.claude/skills/update-dependencies/check-outdated.py
The script never suggests a downgrade. For packages currently on a stable version it finds the highest stable version. For packages currently on a pre-release version it finds the highest semver on the same major — which handles both newer pre-releases and graduation to stable (e.g. 56.0.0-preview.x → 56.0.5).
When the highest stable version is on a newer major than the current one, the script also reports the highest version reachable within the current major as a separate (in-major) line, with the major jump flagged below it:
@babel/core: 7.29.7 -> 7.30.0 (in-major)
↳ MAJOR jump available: -> 8.0.1 (major 7 -> 8)
Take the (in-major) bump as the safe routine upgrade; treat the ↳ MAJOR jump as an opt-in decision (peer-dep checks, app build to verify). If no in-major upgrade exists (already on the latest minor/patch of the current major), only the single cur -> latest line prints — the jump to the next major is then the only available upgrade.
Note on eslint-plugin-react-compiler rc versions: npm may have rc.1-hash variants that sort after rc.2 alphabetically but are older. Verify manually if the script suggests downgrading to a hash-tagged rc.
3. Edit package.json with exact versions
- Use exact versions only — no
^or~ - Edit
shared/package.jsondirectly - Update only the packages you intend to change
- Verify no
^or~are present before saving
4. Install and validate
cd shared && yarn
yarn lint
yarn tsc
Lint/tsc failures after a dep update are caused by the update — do not try to prove they are pre-existing. The branch is clean before the update starts, so any new errors are ours to fix. Fix them before proceeding. If the failures are large or unclear, stop and ask for guidance rather than guessing.
4d. iOS pods — clean when native deps changed
Plain pod install only re-integrates the Pods project; it leaves ios/build/ (stale .o, .pcm module cache, generated headers) and ios/Pods/ framework/header caches from the previous versions. Xcode's incremental build trusts those by timestamp and then compiles/links against headers and symbols that moved when a pod's source was swapped underneath it → build fails out of the box. This bites almost every time a native dependency version changes.
A native dep = anything with an iOS pod: any expo/expo-*, react-native, react-native-*, @react-native-*, lottie-react-native, react-native-kb, etc. Pure JS/tooling bumps (webpack, babel, eslint, typescript, immer, lodash, zustand, @types/*) do not need a pod clean.
If any native dep changed, do the targeted clean instead of a plain install:
cd shared/ios && rm -rf build Pods && cd .. && yarn ios:pod:install
This drops both stale caches without nuking the global CocoaPods cache or re-resolving from scratch — it's the reliable fix for the "Xcode won't compile after a bump" case. Keep Podfile.lock (don't delete it) so resolution stays stable.
Escalate to a full clean only if the targeted clean still fails to build (e.g. react-native-kb codegen went stale, or a corrupted global pod cache):
cd shared && yarn ios:pod:clean && yarn ios:pod:install
ios:pod:clean additionally runs pod cache clean --all (global re-fetch of pod source) and wipes react-native-kb/node_modules — heavier and rarely the actual fix, so it's the fallback, not the default.
If only JS/tooling deps changed, a plain yarn ios:pod:install (or skipping pods entirely) is fine.
4a. Check for duplicate package installs
After yarn, run:
cd shared && python3 ../.claude/skills/update-dependencies/check-dupes.py
This finds packages where a nested node_modules contains a newer version than what's installed at the top level — the case where bumping our pin in package.json would let yarn deduplicate. It ignores nested installs that are older (locked by their parent packages, not fixable by bumping our pins).
If duplicates are found: Update the version in package.json to the suggested version, then re-run yarn.
Why this matters: Packages that use React.createContext() or other module-level singletons break silently when installed twice — the provider uses one instance and the consumer reads a different one. Classic symptom: "Couldn't determine focus state. Is your component inside a screen in a navigator?" (useIsFocused from @react-navigation/core).
4b. Security audit
After yarn, run:
cd shared && python3 ../.claude/skills/update-dependencies/check-audit.py
This runs yarn audit --json, dedupes the advisories, and cross-references yarn.lock to suggest the cheapest fix for each one:
-
DIRECT — the vulnerable package is in our
package.json. Bump it there (exact version), runyarn. -
LOCKFILE — transitive, and the patched version satisfies every range that requires it. Delete the listed entry block(s) from
yarn.lock(the entry key line plus its indented lines), then runyarn— it re-resolves to the patched version with nopackage.jsonchange. -
RESOLUTION — transitive, but the patched version is outside the range the parent accepts. Bumping the direct dependency that pulls it in (first segment of the
via:path) is the better fix when possible — but if you already brought direct deps current in steps 1–4, a still-flagged advisory means no released parent fixes it yet (confirm withnpm view <parent> dependenciesonly if you skipped updating that parent). Then add the suggestedresolutionsentry. Prefer scoping it to the vulnerable parent (e.g."xcode/uuid") over a blanket"**/uuid"when some installed copies are already on a safe version — forcing a major-version jump on every consumer risks breaking ones that were fine. If ALL installed versions are vulnerable (theinstalled:line lists every copy), a blanket**/resolution is correct and covers them all with one entry. If multiple advisories suggest different versions for the same module, add ONE resolution entry with the highest version.Yarn 1 gotchas for scoped resolutions: write the scope as
"**/xcode/uuid"— the documented bare"xcode/uuid"form is silently ignored. And a resolution alone does NOT rewrite an existing lockfile entry: delete the stale entry block (e.g.uuid@^7.0.3:) fromyarn.lockand re-runyarnso the range re-resolves through the resolution. Verify by checking the nested install (node_modules/<parent>/node_modules/<pkg>/package.json), not just the lockfile. -
NO-FIX — no patched version published. Don't work around it; report it to the user with the advisory link.
After applying fixes: re-run yarn, re-run this script to confirm clean, and re-run the dupes check (4a) — resolutions can change the dedupe picture. A forced major bump via resolutions runs code the parent package never tested with. "Verify" means: yarn lint + yarn tsc always; if the forced package sits under runtime app code, also build/run the app; if it sits under tooling (test runners, bundler, patch-package), run that tool once if cheap. Either way, explicitly tell the user which packages were force-bumped so they can watch for fallout.
4c. Minimize existing resolutions
Keep the resolutions block as small as possible — every entry overrides yarn's normal resolution forever and goes stale silently. Two failure modes of stale entries: (1) the entry is redundant because all requesting ranges now resolve to a safe version on their own; (2) the entry actively downgrades a parent's newer pin (e.g. a **/uuid: 11.1.1 left in place forced @appium/support's exact uuid@14.0.0 down to 11).
On every dep-update pass, audit each entry (except **/@types/react, which is permanent — see Notes):
- Remove the candidate entries from
package.json. - Delete the forced entries from
yarn.locktoo — find each entry whose resolved version was pinned by the resolution (e.g.serialize-javascript@7.0.7, serialize-javascript@^6.0.2:) and delete the block. This is the mirror of the "resolution alone does not rewrite the lockfile" gotcha in 4b: removing a resolution does NOT make yarn re-resolve either. The forced version lingers inyarn.lock, so the audit still sees the patched version and comes back falsely clean — then some later pass re-resolves the range down to the vulnerable version and the advisory "mysteriously" returns. This exact false negative caused the serialize-javascript/uuid resolutions to be dropped in PR #29417 (audit "clean" against lingering 7.0.7/11.1.1 lock entries) and re-added one day later when the entries re-resolved to 6.0.2/7.0.3. - Run
yarn, then confirm the installed version actually changed (grep -A1 '^<pkg>@' yarn.lockor checknode_modules/<pkg>/package.json) — if it still shows the previously-forced version, the removal test hasn't actually run yet. - Re-run the audit script (4b) and dupes script (4a).
- If both come back clean, the entries were redundant — leave them removed.
- If an advisory returns, that entry is still needed: restore it, bumped to the latest patched version (not just the minimum the advisory names —
npm view <pkg> version), and remember to delete the now-stale unforced lockfile entry again so the resolution takes effect (4b gotcha).
Testing removal is one yarn run — always do it empirically rather than reasoning from lockfile ranges. But "empirically" means verifying the lockfile actually re-resolved (step 3), not just that yarn exited 0.
Known still-needed entries (as of 2026-09), all three because mocha/xcode pin an old range: **/serialize-javascript (mocha, via @wdio/mocha-framework, still pins ^6.0.2 — GHSA-5c6j-r48x-rmvq, GHSA-qj8w-gfj5-8c6v), **/mocha/diff (mocha 11 pins ^7.0.0; mocha 12 moved to ^9.0.0 but @wdio/mocha-framework pins mocha ^11.8.0, so scope the resolution to mocha rather than forcing every diff consumer — GHSA-73rr-hh4g-fpgx), and **/xcode/uuid (xcode pins ^7.0.3, no release since 2021 — GHSA-w5hq-g745-h8pq). Still run the removal test each pass (a parent may finally ship a fix), but expect these to survive it.
5. Evaluate existing patches
After yarn, check whether any patches/*.patch files target a package you just updated — the filename encodes the version (e.g. @legendapp+list+3.0.0-beta.56.patch). For each such patch:
- Run
yarn patch-packageto see if it applies cleanly. - If it fails: the patch still addresses a real issue, but the upstream source has moved. Fix the source files in
node_modulesdirectly (re-apply the intent of the patch), then runyarn patch-package <package>to regenerate it. Delete the old patch file. - If it applies: check whether the fix was merged upstream by searching the updated source for the patched code. If the upstream already has the fix, delete the patch file.
Never rename patch files or hand-edit the .patch file itself. Let patch-package generate the correctly-named file from your source edits.
6. If updating electron
After bumping the electron version in package.json and running yarn, update the download hashes:
cd shared && ./desktop/extract-electron-shasums.sh <new-version>
This regenerates shared/desktop/electron-sums.mts with the correct SHA256 checksums for all platforms.
Other manifests: protocol/ and rnmodules/react-native-kb/
Dependabot also watches protocol/yarn.lock and rnmodules/react-native-kb/yarn.lock. Cover them on each pass — the same scripts work there (check-outdated.py reads the cwd's package.json and skips file:/link:/github: deps automatically).
protocol/
- Direct deps are tiny; the real tree comes from
avdl-compiler(pinned togithub:keybase/node-avdl-compiler#master), which exact-pins transitives (e.g.lodash "4.17.15") — LOCKFILE deletion can't help, so vulnerable transitives needresolutions. Current block:"**/lodash","**/underscore"(underscore is pulled via jison → nomnom, which pins1.1.x). - Verify any forced bump by running the codegen end-to-end:
cd protocol && make clean && makemust exit 0 AND leavegit statusclean outsideprotocol/package.json+yarn.lock— the generated output (json/, go/protocol/, shared TS) must be byte-identical.
rnmodules/react-native-kb/
- Only one real (non-
file:) devDep:react-native-builder-bob. Bob ≥0.43 validates entry fields withrequire.resolve, which needs an explicit extension — that's why"main"is"src/index.tsx"(not"src/index"); don't "clean it up" back to extensionless or thepreparescript (bob build) fails withFound incorrect path in 'main' field. The"react-native"/"source"fields stay extensionless for metro. - Bob's tsc run uses the module's own
tsconfig.json— keep it free of TS-6-deprecated options (nobaseUrl,moduleResolution: "bundler"). - The app consumes the module's
src/directly via thefile:dep + postinstall sync; bob'slib/output is gitignored and unused. After changing the module'spackage.json, runyarn sync:kb-modulesfromshared/thenyarn lint+yarn tsc. - Everything vulnerable here is transitive dev tooling (babel/metro chain). Fix by deleting the vulnerable entry blocks from
yarn.lockand re-runningyarn— the ranges are loose (^), so they re-resolve to patched versions with nopackage.jsonchange.
go/chat/flip/
Has a package.json solely so make can npm i the avdl compiler for one-off codegen. Its package-lock.json was deliberately deleted (2026-07) to kill stale dependabot alerts — don't recreate it; if npm i regenerates one during codegen, don't commit it.
Notes
lodashtypes (@types/lodash,@types/lodash-es) can be updated independently of lodash itself.- Always bump
@types/reactto the latest patch the script finds, even if runtimereactis unchanged. DefinitelyTyped ships type-only patches independent of the react runtime version. When you bump it, update theresolutionsentry (**/@types/react) to the SAME version — multiple installed copies of@types/reactcause type conflicts. @types/react-dom,@types/react-isshould stay in sync with their runtime counterparts — update only if the runtime version changed.- Packages with versions matching the
expoSDK pattern (e.g.,56.x.x) areexpo-*packages and can be updated together. @react-navigation/coremust track what@react-navigation/nativeactually resolves, not just the surface number of the other nav packages.@react-navigation/native(alpha.24) and@react-navigation/core(alpha.15/alpha.16) use different numbering — they are NOT in sync by design. Always check-outdated on@react-navigation/coreand accept upgrades the script finds, even if the number looks unrelated to the other nav alpha versions. Skipping this causes duplicate installs and React context identity mismatches at runtime.
Signals
- GitHub stars
- 9k
- Forks
- 1k
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
update-dependencies- Source
- github.com/keybase/client