Skip to content
Agentmart
Submit a tool

deep-links-and-universal-links

Gates every incoming link: custom schemes, universal links (AASA), App Links (assetlinks), a route allow-list.

Skill by zakariaf

IndexedMIT★ 11
Write a review

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.

Book a call

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.

We use your email only for this newsletter. Privacy

About

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).

No 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 command
  • link-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 saw

From zakariaf/ReactNative-Skills, MIT.

More from zakariaf/ReactNative-Skills

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
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from zakariaf

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.

Goes to the Chalk Labs founders, not the newsletter. Privacy

Need help deploying deep-links-and-universal-links?

CallGet help
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)