SentinelRN
Runtime Integrity & AI Security SDK · React Native
An open-source security SDK that answers the question most mobile security libraries leave unanswered: "can I trust this environment enough to perform this action?" It detects compromised runtimes (root, jailbreak, emulators, hooking, tampering) and inspects AI-bound input for secrets, PII, and prompt injection — returning structured, explainable threat reports instead of bare booleans. Built end-to-end as creator and sole engineer.
Tech Stack
What I Owned
- ·Trust core: risk scoring & policy engine
- ·AI subsystem: secret/PII/injection detection
- ·Native detectors (Kotlin + Swift)
- ·React bindings, monorepo & release tooling
Engineering Focus
- ·One noisy-OR risk engine for every signal
- ·Fail-open — a detector crash is never an app crash
- ·Strict module boundaries, injected detectors
- ·Explainable, structured threat reports
Security
- ·No network, no secret storage, no egress
- ·Secure-by-default (monitor mode, redaction on)
- ·On-device redaction before LLM calls
- ·Honest about uncertainty — probabilistic by design
Performance
An open-source (MIT) security SDK for React Native that answers a question most mobile security libraries leave unanswered: "can I trust this environment enough to perform this action?" It detects compromised runtimes (root, jailbreak, emulators, debuggers, hooking, tampering) and inspects AI-bound input for secrets, PII, and prompt injection — then returns structured, explainable threat reports instead of bare booleans.
Stack at a glance: TypeScript (strict, verbatimModuleSyntax) · React Native (Expo + bare) · Kotlin · Swift · pnpm + Turborepo monorepo · tsup (ESM + CJS + .d.ts) · Vitest (88 tests / 12 suites) · Biome · Changesets · Expo example app (New Architecture)
Links: GitHub · Live site · published as
@sentinelrn/core,@sentinelrn/native,@sentinelrn/react.
1. Executive summary
React Native increasingly powers apps that handle payments, health data, identity, and AI prompts — running on devices that may be rooted, jailbroken, emulated, hooked, or repackaged. The existing ecosystem solves slivers of this: one library detects root, another does secure storage, a third filters PII. Each returns a boolean. None answer the question an application actually has: given everything I can observe, is it safe to do this right now?
SentinelRN is built around four hard problems, and the architecture is essentially the set of answers to them:
| Hard problem | Architectural answer |
|---|---|
| Booleans don't support real decisions | A risk engine that turns weighted signals into a 0–100 score, a coarse level, and a recommended action |
| Device + AI risk are scored inconsistently | One scoring model (Scorable → assessRisk) shared by integrity reports and AI findings |
| Security logic leaks into UI and platform code | Strict dependency rules — core is pure TS; native and React only feed/consume it |
| A failing detector can crash the host app | Fail-open orchestration — provider errors degrade to a clean, empty report |
The codebase is a pnpm + Turborepo monorepo with three published packages
(@sentinelrn/core, @sentinelrn/native, @sentinelrn/react) plus an Expo example
app. The data flow is a single, testable pipeline:
provider → integrity → threat → risk → report → policy → app.
2. My role
Creator and sole engineer, end to end:
- The trust core — risk scoring, threat normalization, the policy engine, and
the cohesive
SentinelRNfacade, all in pure TypeScript. - The AI subsystem — secret/PII pattern detectors, prompt-injection heuristics, overlap resolution, and reversible redaction.
- Native detectors — Android (Kotlin) root/emulator/hooking checks and iOS (Swift) jailbreak/debugger checks, bridged through one provider contract.
- Developer experience — the React layer, secure-by-default configuration, the published monorepo, release tooling, and the example app.
3. Technology stack & why
| Concern | Choice | Why this over alternatives |
|---|---|---|
| Core language | TypeScript (strict) | The engine must be platform-agnostic and tree-shakeable; pure TS runs in Expo, bare RN, and tests with no native dependency |
| Module hygiene | verbatimModuleSyntax + ESM-first | Forces explicit import type, keeps the boundary between types and runtime code unambiguous |
| Bundling | tsup | Emits ESM + CJS + .d.ts from one config — broad consumer compatibility without hand-rolled build steps |
| Monorepo | pnpm workspaces | Strict, content-addressed installs; clean inter-package linking for the three packages |
| Task graph | Turborepo | Cached build/test/typecheck across packages; only what changed re-runs |
| Tests | Vitest | Fast, ESM-native, jsdom for the React hooks; 88 cases keep the scoring math honest |
| Lint + format | Biome | One fast tool replacing ESLint + Prettier; less config surface to maintain |
| Android detectors | Kotlin (TurboModule) | First-class access to Build, Debug, Settings, the package manager, and the filesystem |
| iOS detectors | Swift | Sandbox probes, symlink checks, sysctl/ptrace-style debugger detection, dyld inspection |
| React bindings | React + context | Idiomatic provider/hook surface; the layer holds no security logic, only state plumbing |
| Demo | Expo (New Architecture) | Proves the SDK works end-to-end on a real device with the New Architecture enabled |
| Releases | Changesets | Versioned, reviewable releases across the workspace |
4. System architecture (the big picture)
Layered structure. Everything flows through the core engine; nothing reaches around it. SentinelRN sits between the React Native app and the actions that matter, continuously evaluating the runtime and AI-bound input to produce explainable trust decisions.

Runtime data flow (one pipeline). A platform provider emits loosely-typed
RawSignals → the threat engine normalizes them into complete ThreatSignals
(stable id, default severity/confidence, human message) → the risk engine scores
the set → a ThreatReport is assembled → the policy engine turns the report into
an allow/block PolicyDecision with reasons → the app enforces. The AI path reuses
the same risk math: AIFindings are Scorable, so device risk and prompt risk are
measured on one ruler.
Dependency rules (enforced by design).
core ──> (nothing)
native ──> core (implements IntegrityProvider)
react ──> core (consumes the SentinelRN API)
example ──> core + native + react
Nothing depends on React except @sentinelrn/react. core contains no platform
code — detectors are injected at runtime via the IntegrityProvider interface,
which is what lets Play Integrity, App Attest, or SSL-pinning providers slot in later
without changing the public API.
5. Package entry points & bootstrap
The public surface is deliberately tiny — a single import, a handful of namespaces:
import { SentinelRN } from "@sentinelrn/core";
SentinelRN.configure(...) // optional — secure defaults otherwise
SentinelRN.integrity // runtime-integrity checks
SentinelRN.ai // PromptGuard
SentinelRN.policy // reports → decisions
SentinelRN.redaction // redact / inspectAndRedact
SentinelRN.version
Singleton + factory. createSentinel(config) builds an isolated instance
(invaluable for deterministic tests); the app-wide SentinelRN singleton is just
createSentinel(). The integrity module reads config through a getConfig() thunk,
so a later configure() takes effect live without re-wiring.
Bootstrap order. Native registration happens once at startup, before the first check:
registerSentinelNative(); // wire native detectors into the singleton
SentinelRN.configure({ policy: "warn" }); // optional
registerSentinelNative() simply calls registerIntegrityProvider() on the target
instance — the React SentinelProvider defaults to that same singleton, so
registration done at the entry point is visible everywhere downstream.
6. Feature domains (the modules)
@sentinelrn/core is a set of small, independently-testable modules with a strict
one-directional dependency graph:
| Module | Responsibility | Notably does not |
|---|---|---|
types/ | Shared vocabulary: severity, confidence, reports, findings, config, errors | — |
risk/ | severity + confidence → score, level, recommended action | Know what a "signal" means |
threat/ | raw signals → normalized signals → ThreatReport | Decide app behavior |
ai/ | secret/PII detection, injection heuristics, redaction, PromptGuard | Talk to any LLM provider |
policy/ | reports → allow/block decisions with reasons | Collect runtime information |
integrity/ | orchestration + the IntegrityProvider contract | Contain platform code |
core/ | config resolution + the assembled SentinelRN facade | Contain platform logic |
Each module owns one job. The risk engine, for instance, scores anything with a severity and confidence — it has no idea whether it's grading a jailbreak signal or a leaked API key, which is exactly why the two stay consistent.
7. Data flow & the signal pipeline
This is the heart of the SDK — where heuristic observations become a defensible number.

Normalization. Detectors are allowed to be lazy: a RawSignal can be as little
as { type: "jailbreak", platform: "ios" }. The threat engine fills in the rest
from a per-type defaults table. Those defaults encode the threat model directly:
| Signal type | Default severity | Default confidence | Rationale |
|---|---|---|---|
hooking, tampering | critical | medium | Active runtime manipulation — the most dangerous, but inherently noisy |
root, jailbreak | high | medium | Strong weakening of platform protections; detection is bypassable |
emulator, debugger, mock_location | medium | high | Reliable to detect, moderately concerning |
simulator, developer_mode | low | high | Almost certainly a developer, not an attacker |
Scoring (noisy-OR). Rather than summing weights (which overflows) or taking a max (which ignores corroboration), the risk engine treats each signal as an independent probability that something is genuinely wrong and combines them as a probabilistic OR:
score = (1 − Π(1 − pᵢ)) × 100, where pᵢ = SEVERITY_WEIGHT[s] × CONFIDENCE_MULTIPLIER[c] / 100
SEVERITY_WEIGHT = { low: 12, medium: 35, high: 65, critical: 92 }
CONFIDENCE_MULTIPLIER = { low: 0.5, medium: 0.8, high: 1 }
RISK_THRESHOLDS = { low: 0, medium: 20, high: 50, critical: 85 }

More signals push the score up but it saturates gracefully toward 100 instead of
overflowing. A single high-severity/high-confidence signal lands in high; a
critical signal (or a cluster of high ones) reaches critical. A device is
"compromised" once risk hits high, and the recommended action ladders from allow
→ monitor → warn_user → block_sensitive_action → block_session. hasSignals
distinguishes a genuinely clean device (allow) from a quiet-but-noisy one
(monitor).
Fail-open orchestration. If no provider is registered, or a provider throws,
integrity.check() swallows it and returns a clean, empty report. A broken detector
is a detection gap — never a crash.
8. The AI subsystem (PromptGuard)
PromptGuard inspects AI-bound text on-device and returns an explainable, policy-aware decision. It never sends anything anywhere — SentinelRN protects prompts; it is not an AI SDK.

Detection layers.
- 15 secret detectors — OpenAI/Anthropic/Google/Stripe/SendGrid API keys, AWS
access keys, GitHub tokens & fine-grained PATs, npm tokens, Slack tokens &
webhooks, Twilio SIDs, PEM private-key blocks, JWTs, plus looser
password:/Bearerassignment patterns (captured at group level so the label survives and only the value is redacted). - 5 PII detectors — email, phone, IPv4, SSN (rejecting reserved ranges), and credit cards (validated with a Luhn checksum to cut false positives).
- 10 injection rules — ignore-previous-instructions, reveal-system-prompt,
override-guardrails, DAN-style jailbreaks, unrestricted-persona, fake
[system]/<im_start>markers, verbatim-repeat extraction, and encoded-payload obfuscation.
Overlap resolution. Detectors run in priority order — secrets beat PII beat injection markers. Overlapping spans are resolved by start index, then priority, then length, and survivors are re-sorted into reading order. So a JWT that also matches a generic email-ish pattern is reported once, as the JWT.
Reversible, structured redaction. Sensitive values are replaced from the end of
the string backward (so earlier indices stay valid) with stable tokens like
[REDACTED_CREDIT_CARD]. Findings carry a masked preview (abc•••••), never the raw
value, unless the app explicitly opts in. Injection findings are reported but not
redacted — the surrounding text is the payload, so policy (not redaction) decides
whether to block it.
Policy-aware decision. Each finding argues for an action (secrets/PII → redact;
high-severity injection → block; otherwise warn); the strongest wins. The four
policies then arbitrate: monitor/warn never block, block stops high-severity or
block-worthy content, and strict blocks on any finding at all.
9. Native integrity detectors
The native package is the only place platform code lives. Both platforms emit a flat
boolean integrity snapshot; the JS provider maps each set flag to a RawSignal
and lets the core threat engine assign severity/confidence — so scoring stays
identical across platforms.
Android (Kotlin). Root detection is layered — su binary paths, which su,
known root & cloaking packages, busybox, test-keys build tags, writable system
paths, and ro.debuggable/ro.secure build props. Plus emulator fingerprinting
(goldfish/ranchu/Genymotion/generic builds), debugger attachment, developer mode,
mock location, hooking, and tampering.
iOS (Swift). Jailbreak detection combines known filesystem paths, a
write-outside-the-sandbox probe, suspicious symlinks on /Applications, and
canOpenURL checks for cydia:///sileo:///zbra:// schemes — short-circuiting to
false on the simulator. Plus simulator, debugger, and hooking checks.
Graceful fallback. If the native module is absent (e.g. Expo Go, web), the provider degrades to weak JS-only heuristics rather than throwing — and the docs are explicit that real integrity detection requires the native module.
10. State management & configuration
There is no global mutable state beyond a single registered provider per instance.
Configuration is resolved by merging a partial config over a base, producing a
fully-populated ResolvedConfig — and the defaults are deliberately the safe
choice:
DEFAULT_CONFIG = {
policy: "monitor", // observe before you block
integrity: { includeEvidence: false }, // don't leak raw evidence
ai: { includeFindings: false, redactMatches: true }, // mask, don't expose
};
Policies are normalized once (resolvePolicy) so a bare string like "block" and a
full object both produce the same shape downstream, with per-mode flag defaults
(block turns on blockOnHighRiskDevice and blockPromptInjection). The React
layer holds the only ephemeral state — useDeviceIntegrity tracks
report/loading/error with a mounted-ref guard so a check that resolves after
unmount can't set state on a dead component.
11. Observability: explainability & reporting
SentinelRN's "observability" is inward-facing: instead of emitting telemetry, it
makes every decision self-explaining. A ThreatReport carries the score, level,
the full list of signals, the recommended action, and a timestamp. A PolicyDecision
carries allowed, the mode, the recommended action, and a reasons: string[] array
spelling out exactly why — e.g. "Risk level: high (score 71).", "Detected signals:
root, hooking.", "Blocked by policy (mode: block)."
This is the anti-boolean stance made concrete: an engineer reading a blocked action in a log can reconstruct the entire decision without a debugger. Raw evidence and raw matches are opt-in, so the explainability never becomes a leakage vector.
12. Error model & fail-open behavior
Two principles govern failure:
- The SDK never crashes the host app. Native detector failures are caught in Kotlin/Swift and reported as "not detected"; provider failures are caught in the integrity orchestrator and degrade to a clean report.
- Errors are structured, not thrown into user space. The
SentinelErrorshape (code,module,message,cause?) tags which subsystem failed (integrity|ai|policy|redaction|native) so callers can handle it uniformly.
The guiding rule: a security tool that takes down the application has a worse failure mode than the threats it's meant to catch.
13. Security posture
This section is intentionally balanced — strong foundations, with an honest hardening backlog.
Strengths
- No data egress. Everything runs locally; no prompts, signals, or secrets ever leave the device through SentinelRN.
- Secure-by-default config. Monitor mode, no raw evidence, redaction on — the easiest path is the safe path.
- No secret storage. The SDK detects secrets; it never persists them.
- Honest uncertainty. Probabilistic detection is labeled with confidence; the docs explicitly forbid "guaranteed"/"unhackable" framing.
- Validated detection. Luhn (cards) and reserved-range checks (SSNs) reduce the false positives that erode trust in a security tool.
Hardening opportunities (documented, not hidden)
- Detection is bypassable. Root/jailbreak/hooking detection is an arms race; advanced attackers can hide indicators. The SDK raises cost, it doesn't guarantee.
- Injection heuristics are regex-based. They catch the obvious cases and will miss novel phrasings; they must be paired with server-side AI safety.
- PII patterns are locale-biased. SSN/phone/card patterns lean US-centric and need expansion for global coverage.
- No runtime attestation yet. Play Integrity / App Attest / DeviceCheck would add a hardware-backed signal the current filesystem heuristics can't.
14. Performance engineering
| Technique | Where | Benefit |
|---|---|---|
| Pure-function core | risk/, threat/, policy/, ai/ | No allocations beyond results; trivially memoizable and tree-shakeable |
| Single-pass detection | detectCandidates | Each detector runs once over the input; overlaps resolved in-memory |
| Backward redaction | applyRedactions | Replacing from the end avoids re-indexing the string per match |
lastIndex reset guard | pattern detectors | Shared global regexes are reset defensively to avoid stateful misses |
| Snapshot-once native bridge | getIntegritySnapshot | One JS↔native round-trip per check, not one per detector |
| ESM + tree-shaking | tsup build | Apps pull in only the namespaces they touch |
| Lazy provider read | getConfig() thunk | Config changes apply without rebuilding the integrity module |
15. Build, release & tooling
- Turborepo drives
build,test,typecheck, andlintacross the workspace with caching, so CI only re-runs what changed. - tsup emits ESM + CJS + declarations per package from a one-line config.
- TypeScript strict with
verbatimModuleSyntaxkeeps the type/runtime boundary explicit and the public.d.tsclean. - Biome is the single lint+format tool — no ESLint/Prettier split.
- Changesets manages versioning and publish across
@sentinelrn/{core,native,react}; a release workflow is guarded onNPM_TOKEN. - Expo example app (New Architecture enabled) is the manual end-to-end smoke test on real devices.
16. Testing strategy
- Vitest, 88 cases across 12 suites. Coverage concentrates on the logic where a bug is most dangerous: the risk math, threat normalization, policy arbitration, detector precision, injection rules, and redaction correctness.
- Property-style scoring checks. The noisy-OR aggregation is tested for monotonicity and saturation (more/worse signals never lower the score; it never exceeds 100).
- Detector precision + recall. Each secret/PII pattern is tested against both true positives and crafted near-misses (e.g. invalid-Luhn numbers, reserved SSNs).
- React hooks are tested under jsdom with
@testing-library/react, including the unmount-during-check race. - Isolated instances (
createSentinel) keep tests deterministic — no shared singleton state bleeding between cases.
17. API design system
SentinelRN treats its API as the design system. The rules:
- One entry point, a few namespaces —
SentinelRN.integrity / ai / policy / redaction. Concepts, not loose utility functions. - Never return a bare boolean — every result explains what happened, how severe, how confident, and what to do.
- Difficult to misuse — secure defaults, optional config, and a uniform
PolicyDecisionshape across both integrity and AI. - Honest naming —
recommendedAction,compromised,confidence; no marketing absolutes.
The example app's small theme (cards, badges, a policy selector, an integrity card, a PromptGuard card) exists only to render these structures — proving the report shapes map cleanly onto a real UI.
18. Key decisions & tradeoffs (summary)
| Decision | Why | Tradeoff accepted |
|---|---|---|
| Structured reports over booleans | Apps need decisions, not flags | More surface area to learn than isRooted() |
| One risk engine for device + AI | Consistent meaning of severity everywhere | Scoring weights must serve two domains at once |
| Pure-TS core, injected detectors | Testability + extensibility without API churn | Real detection requires the native package wired up |
| Noisy-OR scoring | Graceful saturation, rewards corroboration | Weights are hand-tuned heuristics, not learned |
| Fail-open orchestration | Never crash the host app | A silent detector failure looks like a clean device |
| Secrets beat PII beat injection on overlap | Most-sensitive classification wins | Occasional under-reporting of a lower-priority overlap |
| Heuristic injection rules | Ship useful coverage now, on-device | Misses novel attacks; needs server-side backup |
| Monitor as the default policy | Observe before blocking real users | Out-of-the-box, nothing is actually blocked |
19. Known hardening items
A candid backlog of what would make SentinelRN production-grade beyond v0.1:
- Runtime attestation — Play Integrity API, Apple App Attest, DeviceCheck for hardware-backed signals.
- Frida / hooking depth — dedicated instrumentation-framework detection beyond the current heuristics.
- App-signature validation — detect repackaging via signing-cert checks on Android.
- Global PII coverage — non-US phone/ID/card formats and configurable locale packs.
- Injection robustness — move beyond regex toward layered/semantic detection, with explicit server-side guidance.
- SSL-pinning & secure-logging helpers — close the "sensitive data leakage" threats the model documents but the MVP doesn't yet cover.
- OWASP MASVS mapping — trace each feature to a recognized mobile-security control.
- Expo config plugin — one-step native install so registration can't be forgotten.
SentinelRN is risk-based by design: detection is probabilistic and explainable, never a guarantee. The client reduces risk; the server must enforce trust.