Hey Developer, You Might Already Be Compromised
An unexpected sign-in code does not mean you’ve been hacked, but it does mean someone is knocking. This hands-on guide shows developers how to find out whether anything got in, from GitHub keys and cloud credentials to hostile code on your own laptop, and how to clean up in the right order.
Written by Victor Ohachor on
On this page
This morning I revoked a GitHub SSH key.
Nothing had exploded. There was no ransom note and no red screen, only the uncomfortable thought that a key I had used for months might be sitting somewhere I could no longer vouch for.
Then I revoked another; one I had created to enable a GitHub Action workflow auto-deploy to Hetzner for one of my side projects, CleanRoom, though the backend has been down for sometime.
Around yesterday evening, Microsoft had been emailing me one-time codes for sign-in attempts I never started.
I use a lot of cloud services as a developer and I am not oblivious of the risks associated with it. About a week ago, there was a debit attempt on one of my favorite cards I use for online purchases. Luckily, it was too broke to obey the call.
Sometimes back, my Stripe account was hacked and over a thousand invoices sent to customers. This account was not directly mine, but was created for me by my mentor for a startup I built last year. Bad news: I shutdown the startup a few months after the launch month.
Think about the hacks that made the highlights of October. Talk about the kind of companies that were affected. Many of which we rely on as individuals or developers.
I’m not telling you this to be dramatic. I’m telling you because the thing that makes this wave of attacks dangerous is that it is quiet.
I mean...aside from the boisterous wave of advancement in AI and our increasing trust in it for day-to-day use.
Adedeji Olowe (I follow him on LinkedIn and you should too) wrote about the attacks Lendsqr has been fighting since October 30, and his advice to developers fits in three lines: revoke every GitHub token you have ever created, scan your machine, and don’t open projects from people you don’t know. That is good advice. This article is the long version of "scan your machine," because most developers I know would not be able to say what that actually means. So let’s do it properly, hands-on like we developers enjoy it.
Does an unexpected Microsoft OTP mean I have been hacked?
Not by itself. An unrequested one-time code means someone typed my email address into a sign-in page and asked for a code. They reached the door. They did not necessarily get in.
What matters is what else is happening around it. Think of my situation in three levels:
| Level | What happened | Typical evidence | Expected response |
|---|---|---|---|
| Knocked | Someone tried to log in | OTP emails, password reset emails, failed sign-in alerts | Tighten the account, check recent activity |
| Exposed | A secret of mine leaked and may have been used | Unknown sessions, new keys or tokens, odd API activity | Audit and rotate everything reachable from that secret |
| Executed | Hostile code ran on my machine | I ran a stranger’s project, an odd install script, unknown processes | Treat the machine as burned and rebuild it |
Most of us live at level one and don’t know whether we have drifted into two or three. The rest of this article is about finding out.
Why shouldn’t I fix things from my own laptop first?
Because if my laptop is the problem, every new password and token I create on it goes straight to whoever is watching it.
Hence, before I rotate anything, I must decide which device I trust. I would use my phone on a different computer for the account checks and the rotations. Only after I have looked at my main machine, and either cleared it or rebuilt it, would I go back to it.
Likewise, I would grab a notes file on that clean device too. Write down what I found, what time I found it, and what I changed. When I need to tell my team or a client what happened, a timeline beats memory every time.
Yeah...yeah...it does feel like work, but it is work that must be done or I would be spoon-feeding myself the consequences of avoiding the work.
What can my machine actually reach?
Before I hunt for an attacker, I would list what I'd be handing them. A developer laptop is usually a ring of keys to everything else. Hence, I would run this from my home directory and read the output with fresh eyes:
ls -la ~/.ssh ~/.aws ~/.config/gh ~/.docker ~/.kube 2>/dev/null
ssh-add -l
The first line shows which credential folders exist, and the second shows which SSH keys are currently loaded into my agent. Next, I would look for the secrets nobody remembers leaving around:
find ~ -name ".env*" -not -path "*/node_modules/*" 2>/dev/null
On Windows PowerShell, the same hunt looks like this:
Get-ChildItem -Path $HOME -Recurse -Force -Include .env* -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch 'node_modules' }
Every file this turns up is something I may have to rotate. Add the browser sessions you keep logged in, your password manager if it stays unlocked, and any token in ~/.npmrc or your Git credential helper. This list is your blast radius. It’s usually bigger than you expected, which is the whole point of writing it down.
How do I check what was done from the outside?
I would start where Mr. Olowe says the attacks start, with GitHub. Open github.com/settings/security-log and read the last few weeks. I would look for anything I don’t remember doing, such as a new SSH key, a new personal access token, a new OAuth app, a sign-in from a place I have never been. Then I would visit these pages and delete anything I can’t explain including but not limited to settings/keys, settings/tokens, settings/personal-access-tokens, settings/applications and settings/sessions.
If you have the GitHub CLI installed, you can list your keys without clicking around:
gh api user/keys --jq '.[] | {id, title, created_at}'
Next up is the places an attacker plants themselves so they can come back. On each important repository, check deploy keys and webhooks:
gh api repos/OWNER/REPO/keys --jq '.[] | {id, title, created_at}'
gh api repos/OWNER/REPO/hooks --jq '.[] | {id, config: .config.url}'
A webhook pointing at a URL I have never seen is a very bad day, and a very useful discovery.
I would do the same for my email. In Microsoft, open your account security page and read the recent activity. Then go into Outlook’s mail settings and look at rules and forwarding. Attackers love adding a quiet forwarding rule, because it keeps feeding them password resets long after you’ve changed your password. While you’re there, check your sign-in methods and recovery options. If you find a phone number or email address you don’t recognize, you’ve just learned something important.
Then the cloud. If you use AWS, the credential report shows every key and when it was last used:
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d
If a key shows recent use that you can’t explain, search CloudTrail for what that key did:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=YOUR_KEY_ID
Other providers have their own versions of this. The question is always the same: which credentials exist, and which of them have been used by someone who wasn’t me?
How do I check my repositories and pipelines?
Mr. Olowe describes attackers using stolen tokens to plant code that quietly copies every secret out of a company’s pipelines. So your CI configuration deserves more suspicion than your application code. Look at what changed in your workflow files recently:
git log --all --since="60 days ago" -p -- .github/workflows
I would be hunting for new steps that call out to unfamiliar hosts, base64 blobs, or anything that prints or sends environment variables. A rough first pass:
grep -rnE "curl|wget|base64|ngrok|webhook" .github/workflows
Be careful about trusting commit authors here. Author names and emails are trivially forged, so a commit that says it came from you proves nothing. Look at what GitHub itself recorded, such as workflow runs you did not trigger:
gh run list --limit 30
If you publish packages, also check your registry. npm token list shows your active publish tokens, and your package’s version history should contain nothing you didn’t release.
How do I look for hostile code on my own machine?
You finally asked!
If you ran code from a stranger, a recruiter’s test project, or an installer you can’t account for, assume LEVEL THREE and go straight to rebuilding. Mr. Olowe points out (and I repeat) that in some of these projects, merely opening the folder in VS Code is enough. The usual mechanism is a task that runs on folder open, so search every project you’ve opened recently:
grep -rn "folderOpen" ~/projects --include=tasks.json
That’s also why VS Code asks whether you trust the authors of a folder. If you have been clicking yes to make the prompt go away, today is the day to stop. Restricted Mode exists for exactly this case.
Now I would look for persistence, which is how an intruder survives a reboot. On macOS:
ls ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons
On Linux:
crontab -l
systemctl --user list-unit-files --state=enabled
On Windows:
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } | Select-Object TaskName, TaskPath
Get-CimInstance Win32_StartupCommand | Select-Object Name, Command, Location
I would read my shell startup files (~/.bashrc, ~/.zshrc, ~/.profile) for lines I don’t recognize. I would check ~/.ssh/authorized_keys for keys I never added. Then look at my Git config, because hooks and URL rewrites make a sneaky hiding place:
git config --global --list --show-origin
Anything odd under core.hooksPath or url.*.insteadOf deserves a hard look.
Finally, I would see who my machine is talking to right now. On macOS or Linux:
lsof -i -P -n | grep ESTABLISHED
On Windows:
Get-NetTCPConnection -State Established | Select-Object RemoteAddress, RemotePort, OwningProcess
I would then take any process ID that looks unfamiliar and look it up with Get-Process -Id <pid> on Windows, or ps -p <pid> elsewhere. If it’s talking to a server you can’t explain, that’s your answer.
To be honest about the limit of all this: You can find evidence of compromise this way, but you cannot prove a machine is clean. If you ran something you shouldn’t have, the safe move is to wipe and reinstall, and no amount of poking around replaces it.
In what order should I clean up?
The order matters, and it surprises people. A password change does not kill your GitHub tokens, your SSH keys or your OAuth grants. Those survive happily. So within each account, remove the things that outlive a password first, then change the password, then sign out every session. In practice:
- From your clean device, delete unknown SSH keys, tokens, authorized apps, forwarding rules and recovery methods.
- Change the account password, then sign out of all sessions.
- Move to passkeys or a hardware key for two-factor authentication, because SMS codes are the weak link attackers lean on.
- Rotate secrets starting with whatever can move money or destroy data (cloud admin keys, production databases, payment keys), then CI secrets, then everything else on your blast radius list.
- If hostile code may have run, wipe the machine. Reinstall, then re-clone your repositories fresh and don’t copy over dotfiles or
.gitfolders from the old system. - Tell the people who need to know: your team, your clients, anyone whose systems your keys could touch.
Before you wipe, save what you can use as evidence: your security log exports, your shell history and the notes file with your timeline.
How do I stop this from happening again?
You will never get to zero risk, but you can make the next attempt fail.
Open untrusted projects somewhere disposable, such as a virtual machine, a dev container or a cloud workspace, never your main laptop. Turn off install scripts by default with npm config set ignore-scripts true and allow them deliberately. Use fine-grained GitHub tokens with short expiry dates rather than a classic token that lives forever. Protect your SSH key with a passphrase, or better, move to a hardware-backed key with ssh-keygen -t ed25519-sk.
On the pipeline side, require review for any change under .github/workflows using CODEOWNERS, pin third-party actions to a full commit SHA, restrict the default GITHUB_TOKEN permissions, and use OIDC to reach your cloud in place of long-lived keys sitting in CI secrets.
Then go looking for the secrets you’ve already leaked into your own history. Run a scanner like gitleaks or trufflehog over your repositories and see what falls out:
gitleaks detect --source . -v
Do it once, and you’ll never again assume your repository is clean.
What’s the point of all this?
Like Mr. Olowe stated, these attackers succeed because the people they hit stay quiet, so the next company walks into the same trick. The same goes for individual developers. If you find something, tell your team, tell your clients, and write it down publicly if you can. The checks above take an afternoon. The silence is what costs months.
Go open your GitHub security log now. I’ll wait.
Questions people actually ask
Does an unexpected Microsoft OTP mean my account has been hacked?
Not by itself. An unrequested one-time code means someone entered your email address on a sign-in page and asked for a code, so they reached the door without necessarily getting in. It becomes serious when it comes with other signs, such as unknown sessions, new keys or tokens, or forwarding rules you didn’t create.
How can I tell if my developer machine has been compromised?
Look for evidence in four places: unknown SSH keys, tokens and OAuth apps on your accounts, unfamiliar changes to your CI workflow files, odd startup items or scheduled tasks on your machine, and network connections to servers you can’t explain. You can find signs of compromise this way, but you can never prove a machine is clean.
Should I change my passwords first?
Not first. A password change does not remove your GitHub tokens, SSH keys or OAuth grants, hence an attacker who holds one of those keeps access. Inside each account, delete unknown keys, tokens, apps, forwarding rules and recovery methods first, then change the password, then sign out of every session.
When should I wipe my machine instead of cleaning it?
Wipe it if you ran a stranger’s project, a recruiter’s test task or an installer you can’t account for. After reinstalling, re-clone your repositories fresh without copying over dotfiles or .git folders from the old system, and save your logs and notes as evidence first.
Which secrets should I rotate first?
Start with whatever can move money or destroy data, such as cloud admin keys, production databases and payment keys. Rotate your CI secrets next, and then everything else on your list of what the machine could reach.