Join our Newsletter — 33% off our NHI Course

Why do keep rules often fail on the first attempt in protected Android builds?

They fail because static analysis cannot always infer which symbols are accessed dynamically at runtime. Libraries may also hide reflection behind abstractions, so the first rule often fixes only the visible crash while leaving other accessed members renamed or removed. Iteration or profiling is usually required.

Why This Matters for Security Teams

Protected Android builds tend to fail on the first keep-rule pass because the problem is not just “missing a class name.” Static analysis can only see part of the call graph, while reflection, JNI bridges, generated code, and library abstractions can still reach symbols that look unused. That makes the first rule set a partial fix, not a complete one. This is why Android hardening often needs a test-and-refine loop, similar to how secrets programs improve after exposure is measured in the wild, not assumed in policy. NIST’s NIST Cybersecurity Framework 2.0 emphasizes continuous improvement, and the same principle applies to build protection. NHIMG research on The State of Secrets in AppSec shows how often security assumptions diverge from operational reality, especially when controls depend on perfect visibility. In practice, many security teams discover broken keep rules only after a release build strips a runtime dependency that testing did not exercise.

How It Works in Practice

Keep rules are usually written to preserve classes, methods, or fields that a shrinker would otherwise remove or rename. On protected Android builds, the first pass commonly protects the obvious entry points but misses indirect references. That happens because runtime access is often hidden behind reflection, dependency injection, serialization frameworks, or vendor SDK wrappers. The safest approach is to treat the first rule set as a hypothesis, then validate it against actual runtime behavior.

A practical workflow usually looks like this:

  • Start from crash traces, stack traces, and mapping files to identify the exact symbol that failed.
  • Trace from that symbol outward to find reflective calls, annotations, generated bindings, and JNI touchpoints.
  • Protect related members, not just the single failing class, because adjacent members are often accessed by convention.
  • Rebuild, run the protected binary, and compare runtime coverage against the unprotected build.
  • Iterate until the shrinker preserves only what the app truly uses.

This is consistent with Android’s own guidance on code shrinking and obfuscation in the Android developer documentation, which makes clear that shrinkers operate from what they can statically prove. For broader governance of build-time controls and release integrity, NIST CSF 2.0 is a useful anchor, while NHIMG’s Schneider Electric credentials breach illustrates how hidden dependencies and incomplete visibility can create operational surprises in production systems. These controls tend to break down in apps that rely heavily on reflection, code generation, or split APK delivery because the reachable symbols are determined late and vary by device, flavor, or backend configuration.

Common Variations and Edge Cases

Tighter keep rules often increase build complexity and maintenance overhead, requiring teams to balance runtime safety against app size, obfuscation strength, and upgrade effort. The usual tradeoff is that over-preserving code reduces shrinker effectiveness, while under-preserving code causes release-only failures.

Some environments are more fragile than others. Libraries that use JSON serializers, dependency injection, WebView bridges, or plugin architectures may require broader preservation than a simple app module. Guidance is also uneven here: current best practice suggests validating rules against representative release builds, but there is no universal standard for how much runtime coverage is enough. That is especially true when apps ship multiple flavors or dynamically loaded features, because a rule that works in one build variant may fail in another.

Security teams should also avoid assuming that “the first fix worked” just because the app launches. Hidden accesses can surface later in a rarely used flow, a regional code path, or a background task. For teams comparing build hardening approaches, the key takeaway from NHIMG’s broader research on application security is that visibility gaps are usually discovered after failure, not before. In this context, iterative testing is not a tuning exercise, it is part of the control itself.

Standards & Framework Alignment

This section maps relevant standards and security frameworks to the operational risks and controls described in this guidance.

OWASP Non-Human Identity Top 10, CSA MAESTRO and OWASP Agentic AI Top 10 address the attack and risk surface, while NIST CSF 2.0 and NIST AI RMF set the governance and control requirements practitioners need to meet.

Framework Control / Reference Relevance
NIST CSF 2.0 PR.IP-1 Keep-rule tuning is a secure development and change-validation activity.
OWASP Non-Human Identity Top 10 NHI-04 Dynamic runtime access mirrors hidden dependency risk in protected builds.
NIST AI RMF Iteration and evaluation map to measurement and monitoring of system behavior.
CSA MAESTRO GOV-02 Build protection depends on governance for runtime-sensitive dependencies and change control.
OWASP Agentic AI Top 10 A4 Dynamic symbol use and hidden execution paths resemble runtime ambiguity risks.

Validate protected builds continuously and treat keep-rule updates as controlled secure-development changes.