Skip to content

Inside Claude's Android app: Finding the Internal Settings

By Nima Ahmadi

I like taking apart Android apps that catch my attention to see how they work. It’s something I explored in my M.Sc. thesis, too.

With so much attention on Anthropic and Claude Code, I was curious about how their Android app was built. Anthropic has written a lot about Claude and Claude Code, but I couldn’t find much on their blog about the engineering behind the Android app. So I decided to unpack it and take a look myself.

One of the first things that stood out to me was a SharedPreferences key called is_ant.

Following that trail, I found a hidden internal settings menu used by Anthropic engineers to test, debug, and configure the app during development.

This post follows that trail and explores what was inside.

What survives optimization

Android release builds often use R8 or similar tools to shrink, optimize, and obfuscate code. R8 can remove unreachable code and shorten class, field, and method names. Those shorter names reduce the size of the app and also make decompiled code harder to follow. But even after that, a ton of useful information can survive this step.

SharedPreferences keys are one of my favorite places to start. Unlike class and method names, these strings often remain readable. Tracing where a key is used often tells you what it controls.

Following the Ant 🐜

I found about 100 preference keys in the app. Several were interesting, but one immediately stood out: is_ant.

The code that set is_ant to true checked whether the account’s email ended in @anthropic.com. This strongly suggested is_ant was a local indicator for Anthropic employee accounts.

Here’s a Kotlin reconstruction of the logic:

fun updateAccount(account: Account) {
    if (account.email.endsWith("@anthropic.com")) {
        sharedPreferences.edit()
            .putBoolean("is_ant", true)
            .apply()
    }

    // Remaining account setup...
}

Next, I followed where is_ant was read. One of those reads controlled whether an entry appeared in the Settings screen. In the same simplified form:

if (sharedPreferences.getBoolean("is_ant", false)) {
    showInternalSettingsEntry()
}

Seems like we’ve found the internal settings entry point! The next question was whether the screen behind that entry had survived optimization too.

Opening the internal settings

On a rooted test device, I could have changed the stored preference directly. I was exploring other parts of the app at the same time, though, so I decided to repackage it instead: unpack the APK, modify it, rebuild it, and sign the rebuilt APK so I could install it on my test device.

I like to start by changing something obvious. It confirms that the rebuilt app actually contains my changes and makes it easy to tell apart from the original. In this case, I changed the “New chat” label. It worked!

Claude with the modified New chat label
Changing a label to verify the repackaged app.

Next, I changed the condition guarding the internal settings entry so it always passed. After rebuilding the app and opening Settings, the entry point appeared.

Internal settings entry in Claude's settings
The internal settings entry after changing the local condition.

Seeing the entry point didn’t necessarily mean the screen behind it had survived optimization. Luckily, in this case, it had!

A tour of the Internal Settings

The menu had several tools for developing and testing the app. Here’s a slow scroll through the top level.

A tour of the top-level internal settings.

Internal Settings bubble

Enabling “Internal Settings bubble” added a floating, draggable Ant button that opened the internal settings. It snapped to the edges of the screen and was fun to move around.

It’s a small convenience, but it saves a trip through Settings when you are repeatedly changing a debug option.

The floating Ant shortcut to internal settings.

Jank overlay

“Show jank overlay” displayed UI performance statistics. This was connected to JankStats and Android’s FrameMetrics API. Having those measurements available inside the app makes it easier to investigate a slow interaction while reproducing it.

The jank overlay during an interaction.

Age signals

“Age Signal override” exposed controls for simulating different age signals.

Age Signal override controls
Overriding age signals for testing.

Push notifications

There was also a dedicated screen for testing push notifications.

Internal push notification testing screen
A dedicated screen for testing push notifications.

Feature flag overrides

I found GrowthBook integration in the app, along with an “Override feature flag” screen for changing local flag values and configurations.

This was one of the more interesting screens to explore. The feature flags can provide clues about experiments and possible future features. The list deserves a closer look in a separate post.

Browsing the available feature flag overrides.

Simulating unreliable networks

The “Network Simulation” screen offered controls for adding latency and forcing request, upload, and timeout failures.

Network Simulation controls for latency and failures
Simulating unreliable network conditions.

Switching the API endpoint

“Select API Endpoint” offered Production, Staging, Localhost, and a custom backend option.

API endpoint selector showing Production, Staging, Localhost, and Custom options
Switching the app's API endpoint.

The staging hostname was publicly reachable, but Cloudflare Access sat in front of it and required authentication.

Cloudflare Access login gate for Claude's staging environment
The staging environment is gated by Cloudflare Access.

There’s more here than I could fit into one post, especially in the feature flag overrides. I’ll save that rabbit hole for another time. For now, these screens offer a small glimpse into some of the tooling used by Anthropic’s Android team.