sdkmanager exits 0 when it installs nothing
Google's Android package manager prints a licence, reads end of input, installs nothing and returns success. Three packages, none of them real.
4 min readDivyansh Singh
Ward runs local Android emulators beside browser profiles, which means Ward has to be able to obtain an Android SDK for an operator who does not have one and should not be sent to a terminal to fix it. That work produced the most expensive false success I have measured in this project.
The report from the owner was "the repair starts the download and never finishes". What was actually happening was worse than a hang, and it took reading the transcript rather than the exit code to see it.
What was measured
A fresh SDK directory, Google's real repository, the real command-line package manager, three packages requested. The installer reported all three installed in 3.3 seconds. A sweep for the four programs those packages are supposed to provide then failed, and the operator was told that everything had downloaded but the phone emulator did not work on this computer.
Both halves of that sentence were false. Nothing had downloaded, and there was no reason to believe anything about this computer.
Here is the sequence, from the tool's own output:
[licence text for the Android SDK agreement]
Accept? (y/N):
Skipping following packages as the license is not accepted:
Android SDK Platform-ToolsThen it exits. With status 0.
Ward attaches nothing to that process's standard input, on purpose: a licence agreement must be read by the person it binds, and a program that answers y on their behalf has forged a signature. The tool reads end of input, treats it as a refusal, prints the skip line, and reports success anyway.
Why the exit code is the wrong thing to read
There is a defensible reading of this behaviour. "I was asked to install packages, I declined some of them for a reason I printed, nothing went wrong" is not obviously an error condition. Making it exit non-zero would break every script that installs a mixed list and tolerates skips.
That does not help the caller. From outside the process, an install that was refused and an install that succeeded are byte-identical: exit 0, some output on stdout, control returns. The only difference is in prose the tool printed, in the middle of a licence agreement, on a stream most callers redirect to a log they never open.
Two changes came out of this, and they generalise past Android.
Read what the tool said, not what it returned. There is now a parser for the package manager's output whose entire job is to notice the skipped-for-licence line and report a refusal that the exit code did not carry.
Then check the disk. Even a tool that reports honestly can be interrupted, and a package that was requested is not a package that arrived. Each package is verified by looking for the files it is supposed to have produced, under the SDK directory Ward owns. An install is complete when the artefacts exist, not when the installer returns.
The licence digest is not the digest of the licence
The same afternoon produced a second finding, which is the part that would have cost somebody a day.
The agreement Google publishes in the repository manifest and the agreement the tool requires you to have accepted are the same words and different bytes. The published android-sdk-license hashes to efa68a6b. The tool accepts only 24333f8a, which is the SHA-1 of its own copy: four single newlines turned into spaces, one double space collapsed to one, and a trailing newline removed.
Every published manifest version from 2-1 through 2-4 carries identical text, so this is not a stale document that a newer manifest fixes. It is the tool normalising whitespace before hashing, and the normalisation is not written down anywhere.
The wrong answer is to reimplement that normalisation. A guess at somebody else's whitespace rule is what failed here in the first place, and it failed silently. The right answer is to never guess: take the agreement text out of what the tool actually printed, confirm it is the same agreement the operator was shown and accepted, and record the digest of that. If Google changes the normalisation next year, the code keeps working, because it never encoded the rule.
What to take from this
Three things, in decreasing order of how much they will save you.
- An exit code is a claim, not a measurement. Verify the artefact. This applies to every installer, package manager and build tool you shell out to, and it applies hardest to the ones that are usually right.
- Drain the child's output on its own thread and apply the timeout to the wait. The first version of this code read to end of stream before applying its ninety minute ceiling, so a tool that stopped printing and never exited parked there forever and the ceiling was never reached.
- Take the tail of the output, not the head. These tools open with a banner - the package manager's is four lines about being deprecated - and the error is on the last line. A head-first diagnostic reports the banner every time and the error never.
None of this is Android-specific. It is what happens whenever a program's success is inferred from the fact that another program came back.
How this post was checked
- Data source
- Google's live Android SDK repository and the real command-line package manager, run on Windows 11 against a fresh SDK directory on 9 August 2026
- Measured with
- The Android SDK command-line tools, revision 22.0, driven by Ward's installer with nothing attached to the child process stdin
- The claim
- The package manager returns exit code 0 after declining to install every package it was asked for, and only its stdout says otherwise.
Divyansh Singh
Builds Ward at Digital Heroes
Divyansh Singh builds Ward, a Windows manager for many isolated browser profiles, at Digital Heroes. Most of his week is spent in the Chromium command line, the DevTools Protocol and Windows process behaviour. Every measurement quoted in these posts was taken on the machine Ward is built and tested on, with the browser or tool version written down beside the result, and the ones that contradicted what the documentation said are the ones that became posts.
Last reviewed . Corrections and bug reports: support@digitalheroes.co.in.
Ward is the desktop application these measurements came out of
Many browser profiles on one PC, each with its own proxy and its own device identity. Free plan, free account, nothing to buy today.