deep-links-and-universal-links
Gates every incoming link: custom schemes, universal links (AASA), App Links (assetlinks), a route allow-list.
Skill by zakariaf
Chalk Labs
Need help deploying deep-links-and-universal-links?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/zakariaf-deep-links-and-universal-links
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 26, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Node.js package
Network hosts it talks to 2
codeload.github.com· found in install commandlink-check.invalid· found in code
Files it touches
~/.claude/skills/deep-links-and-universal-links
What's inside
- Skill name
deep-links-and-universal-links- Description
- Gates every incoming link: custom schemes, universal links (AASA), App Links (assetlinks), a route allow-list. Use when adding a deep link, scheme, associated domain or share link. Not for routes (navigation-and-routing).
- License
- MIT
- Instructions
- 195 lines in SKILL.md · view on GitHub
Before installing
- Ships executable scripts
Files (81)
- SKILL.md
- shared.json · assets/
- app-config-deep-links.ts · examples/
- link-routes-tasks.ts · examples/
- maestro-deep-links.yaml · examples/
- share-task-link.ts · examples/
- checker.md · references/
- device-testing.md · references/
- link-routing.md · references/
- react-navigation-linking.md · references/
- universal-and-app-links.md · references/
- check-deep-links.mjs · scripts/
- check-lib.mjs · scripts/
- app-modules.mjs · scripts/lib/
- link-facts.mjs · scripts/lib/
- ship-placeholders.mjs · scripts/lib/
- source-scan.mjs · scripts/lib/
- selftest.mjs · scripts/
- write-well-known.mjs · scripts/
- deep-links.json · templates/contracts/
- deep-links.schema.json · templates/contracts/
- link-native-config.test.ts · templates/scripts/deep-links/
- link-native-config.ts · templates/scripts/deep-links/
- +native-intent.tsx · templates/src/app/
- incoming-link.test.ts · templates/src/core/links/
- incoming-link.ts · templates/src/core/links/
- link-route.test.ts · templates/src/core/links/
- link-route.ts · templates/src/core/links/
- route-incoming-link.test.ts · templates/src/core/links/
- route-incoming-link.ts · templates/src/core/links/
- app-link-policy.test.ts · templates/src/features/deep-links/
- app-link-policy.ts · templates/src/features/deep-links/
- deep-links-navigation.test.tsx · templates/src/features/deep-links/
- link-origins.test.ts · templates/src/features/deep-links/
- link-origins.ts · templates/src/features/deep-links/
- link-routes.ts · templates/src/features/deep-links/
- redirect-system-path.test.ts · templates/src/features/deep-links/
- redirect-system-path.ts · templates/src/features/deep-links/
- use-link-gate.test.ts · templates/src/hooks/
- use-link-gate.ts · templates/src/hooks/
- use-open-pending-link.test.tsx · templates/src/hooks/
- use-open-pending-link.ts · templates/src/hooks/
- link-gates.test.ts · templates/src/services/deep-links/
- link-gates.ts · templates/src/services/deep-links/
- pending-link-inbox.test.ts · templates/src/services/deep-links/
- pending-link-inbox.ts · templates/src/services/deep-links/
- SKILL.md · tests/bases/app/.agents/skills/link-demo/
- planted.ts · tests/bases/app/.agents/skills/link-demo/src/
- app.config.ts · tests/bases/app/
- deep-links.json · tests/bases/app/contracts/
- navigation.json · tests/bases/app/contracts/
- package.json · tests/bases/app/
- SKILL.md · tests/bases/app/skills/link-demo/
- planted.ts · tests/bases/app/skills/link-demo/src/
- index.tsx · tests/bases/app/src/app/(app)/
- [listId].tsx · tests/bases/app/src/app/(app)/lists/
- [taskId].tsx · tests/bases/app/src/app/(app)/tasks/
- new.tsx · tests/bases/app/src/app/(app)/tasks/
- setup.tsx · tests/bases/app/src/app/(debug)/debug/
- _layout.tsx · tests/bases/app/src/app/
- …and 21 more
SKILL.md (first lines)
# Deep links and universal links
Every link that opens the app (a custom scheme, an iOS universal link, an Android App Link; cold start
or warm) passes one synchronous gate that opens only allow-listed routes with checked values. Verified
hosts flow from one contract into the app config of every variant and into the hosted
apple-app-site-association and assetlinks.json files. A checker replays hostile URLs through the app's
own gate. Verified on 2026-10-14 with Expo SDK 57 (expo 57.0.26), expo-router 57.0.24 and expo-linking
57.0.11 in Jest inside the real router and with `npx expo config`; not yet driven on a device.
## Rules that must hold
1. **One gate for every incoming link: `src/app/+native-intent.tsx`.** It exports `redirectSystemPath`,
which hands every URL, cold (`initial: true`) and warm (`initial: false`), to `routeIncomingLink` and
returns only its answer. No other code subscribes to links: no `'url'` listener, `getInitialURL`,
`getLinkingURL`, `useLinkingURL`, `useURL` or `legacy_subscribe`. Why: Expo Router turns every file into
a URL and routes whatever arrives; a second handler navigates twice or opens what the gate refused.
(`intent-gate`, `one-listener`)
2. **The gate is synchronous and total.** The decision runs inside `attempt(...)`; a crash is reported to
the error net and treated as a refusal; no `async`, `await`, `then` or `fetch` in it. Why: a throw there
crashes the launch, a promise holds the first screen, and a rejection loses the link. Short links and
referral codes resolve in a landing route that replaces itself. (`intent-total`, `gate-behaviour`)
3. **An allow-list decides what a link may open.** `LINK_ROUTES` names each linkable route by URL path
(groups dropped, no catch-alls), and every entry is a real route file. Anything else opens the first
screen. Why: without it any web page, QR code or app can open account deletion, checkout or a debug
screen with any params. (`link-routes`, `link-route-exists`)
4. **Values are checked by kind, query keys are listed, the href is re-encoded.** Each dynamic segment has
a kind (`slug`, `uuid`, `int`); unknown query keys (`utm_source`, `fbclid`) are dropped; a repeated kept
key, a control character or a value over 256 characters is refused; no query key shadows a segment
name; the gate returns an encoded path.
The screen still parses its params with its valibot parser. Why: Expo Router decodes query values and
joins them again unencoded (`a%26b` becomes two keys), and in-app navigation never passes the gate.
(`link-routes`, `gate-behaviour`)
5. **A refused cold link lands on the first screen; a refused warm link changes nothing.** Cold returns
`/`, warm returns `null`, and the refused URL is shown nowhere. Why: the router must render something at
launch, but navigating home from a running app throws away unsaved work for a link that opens nothing.
6. **A link into a guarded group waits, then opens once.** Its entry names the `gate` and builds a typed
href in `open()`; the guard's owner calls `useLinkGate(gate, isOpen)`; the first screen behind the guard
calls `useOpenPendingLink(gate)`. Why: on SDK 57 a link into a closed `Stack.Protected` group lands on the
first reachable screen and is lost after sign-in; `+native-intent` cannot read React state.
(`pending-link`)
7. **Custom schemes reach anyone; secrets ride only verified links.** A route whose query or params carry a
token, password, OTP, magic or reset code is `verifiedOnly` (http(s) of a verified host: Android verifies
both schemes, iOS delivers only https). The secret is single-use, short-lived and confirmed on screen.
Why: any app can register the app's scheme and receive the link; any app can still send a verified one.
(`link-secret`)
8. **Links open screens; they never act, and never forward.** No route opened by a link mutates, buys,
deletes, signs in or changes settings on arrival; the person taps. No link's params reach
`Linking.openURL` or the browser, and none becomes an href (`router.replace(params.next)`): a
return after sign-in goes through the pending-link inbox. Why: links are sent by strangers, prefetched by mail scanners and
re-delivered after a reload; a forwarded URL is an open redirect with the app's trust. (`open-redirect`)
9. **Debug and test-build routes are refused in production.** The debug link entry is
`{ path: '/debug/setup', query: 'any', testBuildOnly: true }`. Why: a production link must not reach a
screen the production bundle does not contain. (`link-test-only`)
10. **One contract for verified hosts, applied to every variant.** `contracts/deep-links.json` lists hosts
per variant; `linkNativeConfig()` turns them into `applinks:<host>` entries and one App Links filter
(`autoVerify`, `VIEW`, `BROWSABLE`, `DEFAULT`, http and https, a `pathPattern` per web path); the gate
accepts exactly the hosts of the running config (publish each update with the build's `APP_VARIANT`). Why: when the entitlement, the filter and the gate
disagree, links silently open the browser or a page the app refuses. (`link-contract`, `native-links`)
11. **The web paths and the hosted files match the app.** Every `webPaths` entry opens a listed route; each
host's AASA and assetlinks.json are generated by `write-well-known.mjs` and deployed by a person to
`https://<host>/.well-known/` over HTTPS with no redirect. Why: iOS fetches the AASA through Apple's CDN
at install and about weekly after, and Android verifies at install; a wrong file fails silently.
(`web-paths`, `well-known`)
12. **Real ids and fingerprints before a release.** The assetlinks fingerprint is the key that signs what
people install (Play App Signing's, not the upload key's); `--release` fails on `example.com` hosts,
`com.example.*` ids and unfilled tokens. (`ship-placeholder`)
13. **Every linked route is tested cold and warm.** Jest through the real router with a `+native-intent`
entry (cold via `initialUrl`, warm via the router's `url` listener), then named devices or Maestro
`openLink`. Why: Jest cannot prove the OS hands the link to the app.
## Workflow
1. **Read what the task needs:**
| Task | Read |
|---|---|
| first setup, a new linked screen, params, gates, cold vs warm, notification or OAuth links | [references/link-routing.md](references/link-routing.md) |
| universal links, App Links, AASA, assetlinks.json, fingerprints, hosting | [references/universal-and-app-links.md](references/universal-and-app-links.md) |
| trying links on a simulator, an emulator or in Maestro | [references/device-testing.md](references/device-testing.md) |
| a FAIL line, the replay, configuration | [references/checker.md](references/checker.md) |
| a React Navigation app | [references/react-navigation-linking.md](references/react-navigation-linking.md) |
2. **Set up the gate once.** Copy `templates/src/app/+native-intent.tsx`, `templates/src/core/links/`,
`templates/src/features/deep-links/`, `templates/src/services/deep-links/`, the two hooks in
`templates/src/hooks/` and `templates/contracts/` (every test with its file). The scheme per variant
comes from app-variants-and-environment. With the debug menu (debug-menu-and-test-hooks), add its
test-build entry to `LINK_ROUTES`. Run
`npx jest src/core/links src/features/deep-links src/services/deep-links src/hooks --selectProjects unit`.
3. **Link a screen.** Add its entry to `LINK_ROUTES` after `examples/link-routes-tasks.ts`: the URL path,
a kind per segment, the query keys the screen reads, `verifiedOnly` for secrets. The screen parses its
params (navigation-and-routing's params parser). Behind a guard: add `gate` and `open`, call
`useLinkGate` where the guard is decided and `useOpenPendingLink` in the first screen's model. Add a
case to `deep-links-navigation.test.tsx` for the new route, cold and warm.
4. **Add verified links** (when the app has a domain): fill `contracts/deep-links.json` (hosts per variant,
`webPaths`, `appleTeamId`, `androidCertFingerprints`), wire `templates/scripts/deep-links/link-native-config.ts`
into app.config.ts as `examples/app-config-deep-links.ts` shows (a variant without hosts gets no
`associatedDomains` key: an empty array still writes the entitlement), make a new native build, and hand
off to expo-prebuild-and-config-plugins: refresh its native snapshot with its `--write` option after
reading the drift (the new entitlement and MainActivity filter) and commit it with the change. Then run
`node ${CLAUDE_SKILL_DIR}/scripts/write-well-known.mjs .`. **Stop and ask the developer** to deploy each
`<wellKnownDir>/<host>/` folder to `https://<host>/.well-known/` and to confirm the fingerprints from
Play Console or `eas credentials`; uploading to a website is outward-facing.
5. **Try it on devices** (references/device-testing.md): cold and warm on a named simulator and emulator,
`pm get-app-links` for Android verification; hand the flows to e2e-maestro (`examples/maestro-deep-links.yaml`).
6. **Run the checks:** `node ${CLAUDE_SKILL_DIR}/scripts/check-deep-links.mjs .`; fix every `FAIL` line
(file, rule, fix) and rerun until `RESULT: PASS`. In CI, also
`node ${CLAUDE_SKILL_DIR}/scripts/write-well-known.mjs . --check`. Before a release, run the checker with
`--release`.
## Definition of done
- [ ] `src/app/+native-intent.tsx` re-exports `redirectSystemPath`; no other code listens to links.
- [ ] `LINK_ROUTES` lists every linkable route with kinds and query keys; secrets are `verifiedOnly`, debug
routes `testBuildOnly`, gated routes have `open()`, `useLinkGate` and `useOpenPendingLink`.
- [ ] No linked screen acts on arrival; no link's params reach `openURL` or the browser.
- [ ] The template tests and a cold and warm case per linked route pass in Jest; `redirect-system-path.test.ts`
is what proves rule 5 (refused cold `/`, refused warm `null`), which the checker cannot replay.
- [ ] With a domain: the contract, the app config of every variant and the well-known files agree
(`write-well-known.mjs . --check` prints `RESULT: PASS`); the developer deployed the files and sawFrom zakariaf/ReactNative-Skills, MIT.
More from zakariaf/ReactNative-Skills
- 0.606score
debug-menu-and-test-hooks
zakariaf
Gates test-only code behind one literal TEST_ONLY switch: debug menu, debug link, simulated clock and offline.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
accessibility-as-code
zakariaf
Enforces a11y labels, 44pt targets, 200% text, VoiceOver, TalkBack.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
adaptive-layout
zakariaf
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
admob-ads-and-consent
zakariaf
Builds AdMob ads behind a port - banners, interstitials, rewarded, UMP consent then ATT, pacing, ADS_MODE test IDs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
analytics-and-consent
zakariaf
Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
android-release-play
zakariaf
Ships Android App Bundles to Google Play: upload key, Play App Signing, AAB gate, versionCode, tracks, rollout.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
architecture-and-boundaries
zakariaf
Places app code in the src/ layout and checks import boundaries: one-way zones, pure core, ports with fakes, one composition root.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
async-safety
zakariaf
Prevents silent async bugs: effect races, stale results, lost promises, leaked listeners and timers, AbortSignal gaps.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
audio-and-haptics-feedback
zakariaf
Builds SFX and haptics behind ports.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
auth-and-secure-storage
zakariaf
Builds sign-in - tokens in expo-secure-store, shared refresh, protected routes, OAuth PKCE, Apple sign-in.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
background-tasks
zakariaf
Builds expo-background-task jobs: idempotent, budgeted runs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
canvas-gestures-and-hit-testing
zakariaf
Turns canvas taps, swipes, drags and aim into typed intents: one gesture module, hit-testing on the drawn layout.
SkillAFreshness: A★ 11Claude Code +9
Signals
- Stars
- 11
- Forks
- 2
- Open issues
- 0
- Last commit (this folder)
- Oct 17, 2026 (17 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/zakariaf/ReactNative-Skills
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from zakariaf
- 0.606score
admob-ads-and-consent
zakariaf
Builds AdMob ads behind a port - banners, interstitials, rewarded, UMP consent then ATT, pacing, ADS_MODE test IDs.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
accessibility-as-code
zakariaf
Enforces a11y labels, 44pt targets, 200% text, VoiceOver, TalkBack.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
canvas-gestures-and-hit-testing
zakariaf
Turns canvas taps, swipes, drags and aim into typed intents: one gesture module, hit-testing on the drawn layout.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
adaptive-layout
zakariaf
Lays screens out by window, not device: breakpoints, master-detail panes, readable widths, grids.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
debug-menu-and-test-hooks
zakariaf
Gates test-only code behind one literal TEST_ONLY switch: debug menu, debug link, simulated clock and offline.
SkillAFreshness: A★ 11Claude Code +9 - 0.606score
analytics-and-consent
zakariaf
Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT.
SkillAFreshness: A★ 11Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get deep-links-and-universal-links running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
deep-links-and-universal-links skill. Agent Mart. https://agentmart.dev/skills/zakariaf-deep-links-and-universal-links (last updated 2026-10-24)