I was rebuilding one of my wallpaper apps on EAS late one evening. The build came back green and the artifact downloaded without complaint. Then I opened it on a real device, and Firebase initialization failed quietly, seconds after launch.
The same code ran fine in my local development build. I walked back through the diff for half an hour before it landed: I had put android/app/google-services.json in .gitignore. Keeping it out of history was the thought that came first, and I had forgotten that the build machine still needs it.
EAS Build packs up your project and ships it to the cloud. What decides what does not go is, by default, your .gitignore. A file you decided not to commit is also a file the builder never sees.
.gitignore answers what stays out of history. .easignore answers what reaches the build machine. They share a syntax, which makes them look like the same list, but the question underneath each one is different.
The two lists replace each other — they do not add up
To fix that initialization failure I created a .easignore. I wrote docs/ and dist/ into it, and deliberately did not write google-services.json. My reading was that the native config would now pass through.
It did. Firebase initialized.
The next build's upload also got noticeably slower, because node_modules was going up in full.
.easignore is not added to .gitignore. The moment it exists, it is read instead of .gitignore. Everything you had in .gitignore — node_modules/, .expo/ — stops applying unless you copy it across. If you write the file thinking of it as "a few extra exclusions," this is where it catches you.
Count the bundle before you send it
Noticing this after the upload costs you a full build cycle. So I started counting first. Borrowing git's own matcher takes a handful of lines.
#!/bin/sh
# Pick the single rule file that actually applies, then count what ships
RULES=.gitignore
[ -f .easignore ] && RULES=.easignore
echo "rules in effect: $RULES"
# -o lists untracked files; --exclude-from reads only the file you name
git ls-files -o --exclude-from="$RULES" | wc -l
git ls-files -o --exclude-from="$RULES" -z | xargs -0 -r du -cb | tail -1
echo "--- things you do not want in / things you cannot afford to lose ---"
git ls-files -o --exclude-from="$RULES" \
| grep -E 'google-services|GoogleService-Info|(^|/)\.env|node_modules/|(^|/)\.expo/'The key part is --exclude-from. With that flag git does not pull in the worktree's .gitignore, so you get an honest local preview of the world after .easignore exists.
I ran three rule sets against a representative project layout — src/ and assets/ plus node_modules/, .expo/, dist/, docs/, .env, and two native config files, 18 files in total. The ignore files themselves are left out of the counts.
| Rules applied | Bundled | Total size | What happens |
|---|---|---|---|
| A .gitignore only | 12 files | 5.20 MiB | google-services.json and GoogleService-Info.plist never arrive |
| B .easignore with only docs/ and dist/ | 16 files | 6.82 MiB | native config arrives, but so do node_modules, .expo and .env |
| C rewritten .easignore | 13 files | 4.53 MiB | native config arrives; the heavy things and .env stay out |
B is 1.62 MiB heavier than A. At this scale the number itself looks like noise — what matters is that the extra weight is node_modules and .env. The builder reinstalls dependencies anyway, so shipping them buys nothing. And .env is not merely unnecessary; it is a file I would rather not send at all. How I pass those values instead is in EAS secret visibility does not keep a value out of your app.
C comes out 0.67 MiB below A, which is simply docs/ dropping off. I added what the build needed and the archive still got lighter.
Once the parent is excluded, an exclamation mark will not bring it back
Writing C, my first attempt looked like this:
android/
!android/app/google-services.json
Drop the whole android/ tree, since prebuild regenerates it, and pull one config file back. It felt like the obvious move. It does not work. Here is what the same matcher reported:
| How it is written | google-services.json is |
|---|---|
android/ plus !android/app/google-services.json | still excluded — it never arrives |
android/* → !android/app → android/app/* → !android/app/google-services.json | bundled |
When you exclude a parent directory outright, no exclamation mark inside it can call a file back, because git stops descending at the directory. To bring something back you have to reopen each level on the way down. Writing !node_modules/react/index.js under an excluded node_modules/ was ignored in exactly the same way.
This is where I lost the most time — an exception you believe you wrote simply does nothing, and nothing tells you so.
How I lay out my own .easignore
These days I write it in three blocks with different purposes. The order carries no technical weight, but it stops me hesitating when I read the file again months later.
# 1. Sent but never used — the builder regenerates these
node_modules/
.expo/
ios/Pods/
android/.gradle/
dist/
# 2. Things I do not want to send — values live in EAS environment variables
.env
.env.*
# 3. Simply heavy — design files and source material
docs/
design/
# 4. Native config files are deliberately absent here.
# They stay in .gitignore, and they pass through to EAS.
The fourth block records something I did not write. Left blank, a future version of me reads the file, assumes it was an oversight, and adds the line back. One sentence of reasoning removes that doubt.
I wrote up the matching inventory work on the environment variable side in There is no --json on eas env:list. Deciding whether android/ is safe to drop wholesale rests on the process in Find the native edits expo prebuild will erase before you upgrade to SDK 57.
One small thing before your next build
There is only one thing I would suggest trying. Drop those few shell lines into scripts/ and run them before you type eas build. If the numbers match what you expected, send it and carry on.
The first day I ran it, I found node_modules sitting in the list. Learning that after waiting out a full build, versus learning it three seconds before sending, makes a real difference to what is left of your afternoon.