# Root Jailbreak Detection

> Detecting rooted Android and jailbroken iOS devices as a risk signal — not a silver bullet. Use when deciding which operations to allow on a compromised device.

- Skill: `almasumdev/root-jailbreak-detection` (Agent Skill)
- Install (CLI): `npx skillmds@latest add almasumdev/root-jailbreak-detection`
- Raw SKILL.md: https://api.skillmd.com/api/skills/almasumdev/root-jailbreak-detection/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: almasumdev (https://skillmd.com/u/almasumdev)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/almasumdev/root-jailbreak-detection

---


# Root / Jailbreak Detection

## Instructions

Root and jailbreak detection are **signals**, not security controls. A rooted device can hide itself from any detection you write. Design for that reality.

### 1. What a Rooted / Jailbroken Device Actually Means

- Platform sandbox is no longer a trust boundary for your app.
- Keystore / Keychain material is still protected **if** it is hardware-backed — root can't extract from StrongBox / Secure Enclave.
- TLS pinning can be bypassed (user CA installed into system store, Frida unpinning, LSPosed modules).
- Screen recorders, input injectors, and clipboard snoopers are trivial.

You may still want to run on these devices (your users include pentesters, tinkerers, and kids with unlocked bootloaders) — but with a higher risk score.

### 2. Android Signals (Combine Several)

```kotlin
fun rootSignals(ctx: Context): List<String> {
    val signals = mutableListOf<String>()

    // 1. Common su binaries
    val suPaths = listOf(
        "/system/bin/su", "/system/xbin/su", "/sbin/su",
        "/system/app/Superuser.apk", "/vendor/bin/su",
    )
    if (suPaths.any { File(it).exists() }) signals += "su_binary"

    // 2. Magisk / SuperSU package names
    val rootPackages = listOf(
        "com.topjohnwu.magisk", "eu.chainfire.supersu",
        "com.koushikdutta.rommanager", "com.noshufou.android.su",
    )
    if (rootPackages.any { isInstalled(ctx, it) }) signals += "root_package"

    // 3. Test-keys build
    if (Build.TAGS?.contains("test-keys") == true) signals += "test_keys"

    // 4. Writable system partitions
    val rwDirs = listOf("/system", "/system/bin", "/system/xbin")
    if (rwDirs.any { File(it).canWrite() }) signals += "system_rw"

    return signals
}
```

Each signal is independently bypassable. Report the **set** of signals, not a boolean.

### 3. iOS Signals

```swift
func jailbreakSignals() -> [String] {
    var signals: [String] = []

    // 1. Known jailbreak files
    let paths = [
        "/Applications/Cydia.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash", "/usr/sbin/sshd", "/etc/apt",
        "/private/var/lib/apt/",
    ]
    if paths.contains(where: { FileManager.default.fileExists(atPath: $0) }) {
        signals.append("jb_file")
    }

    // 2. Sandbox escape: can we write outside our container?
    let probe = "/private/jb_probe_\(UUID().uuidString)"
    if (try? "x".write(toFile: probe, atomically: true, encoding: .utf8)) != nil {
        signals.append("sandbox_rw")
        try? FileManager.default.removeItem(atPath: probe)
    }

    // 3. Fork allowed (only jailbroken iOS allows fork)
    if fork() >= 0 { signals.append("fork_allowed") }

    // 4. Known URL schemes
    if let url = URL(string: "cydia://package/com.example"),
       UIApplication.shared.canOpenURL(url) { signals.append("cydia_scheme") }

    return signals
}
```

The `fork()` and sandbox-write checks are the hardest to bypass without kernel patches — the file-path checks are trivial to hide.

### 4. Library Options

- Android: `RootBeer`, `dtx-free` style checks. Read their source, fork, and rename before shipping — stock names are what jailbreak-hiders target.
- iOS: `IOSSecuritySuite`, `DTTJailbreakDetection`. Same caveat.
- Don't trust a single "is rooted?" function. Combine N signals with weights.

### 5. Treat as Risk Score, Not Verdict

Bad pattern:

```kotlin
if (isRooted) { finish(); return } // attacker patches the method, done.
```

Better pattern:

```kotlin
val signals = rootSignals(ctx)
riskScore += signals.size * 10
riskScore += if (attestationVerdict != STRONG) 20 else 0
// Send score + signals to server; let the server decide.
```

The server may:

- Allow read-only usage.
- Block money movement / key operations / document export.
- Require step-up MFA.
- Silently flag for fraud review.

### 6. Business Policy First

Before writing a single check, get written agreement on policy:

- "We allow login on rooted devices, but not transactions over $X."
- "We allow all operations with warning, with extra logging."
- "We refuse to run at all on jailbroken devices."

Each of these is defensible; each has different UX and legal implications. Don't decide this in code review.

### 7. UX

- If you block, tell the user **why** in plain language ("This app requires an unmodified device for security reasons"). Don't hide behind a generic error.
- Provide a support channel for false positives.
- Never ship a hard block without a feature flag to roll back.

## Checklist

- [ ] Root / jailbreak detection combines multiple independent signals.
- [ ] Results are reported as a score + signal list, not a boolean.
- [ ] Server makes the final decision for sensitive operations.
- [ ] Business policy for rooted/jailbroken devices is documented and agreed in advance.
- [ ] Detection libraries are forked / renamed, not used stock.
- [ ] Blocked users get a clear explanation and a support path.
- [ ] A feature flag exists to disable enforcement remotely.

