npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mohitagw15856/undo

v0.1.0

Published

You just ran something you should not have. Paste it here, get the recovery steps. Works offline.

Downloads

101

Readme

entries recoverable partial gone

You just ran something you should not have. Start here.

A recovery registry for destructive commands. Every entry answers the same four questions in the same order, because at 2am you do not want an essay: is it recoverable, how long have I got, what do I type, and how do I stop doing this.

npx github:mohitagw15856/undo "git reset --hard"     # paste what you ran, get the steps

Runs straight from this repo — nothing to install, nothing to clean up. (The bare name undo on npm belongs to an unrelated package, so the published name here is @mohitagw15856/undo.)

Before anything else: stop writing to the disk. Most recovery failures are not because the data was unrecoverable — they are because something overwrote it during the twenty minutes spent panicking. Close the editor, stop the build, and read the entry before you type.

Everything, worst first

| | Command | Recoverable? | You have | What it costs you | | :-: | --- | :-: | --- | --- | | 🔴 | committed a secret | No | — | The secret is in git history, in every clone, and if pushed to a public repo it is being scraped within minutes. | | 🔴 | git clean -fd | No | — | Deletes untracked files and directories. With -x, ignored files too, which includes .env. | | 🟡 | > file | Sometimes | — | The shell truncates the target to zero bytes before the command runs — so a typo destroys the file even if the command then fails. | | 🟡 | answered Yes to a destructive prompt | Sometimes | — | Varies. What matters is that you now need to know what it actually did. | | 🟡 | aws ec2 terminate-instances | Sometimes | — | The instance is destroyed. Root EBS volumes with DeleteOnTermination=true go with it; other attached volumes usually survive. | | 🟡 | aws s3 rm --recursive | Sometimes | — | Deletes objects. With versioning on, it writes delete markers instead of destroying data. | | 🟡 | chmod -R 777 | Sometimes | — | Overwrites every permission bit in the tree. The previous permissions are not recorded anywhere. | | 🟡 | crontab -r | Sometimes | — | Deletes your entire crontab. There is no confirmation prompt, and -r sits directly next to -e on the keyboard. | | 🟡 | dd of=/dev/disk | Sometimes | — | Writes raw bytes over the partition table and filesystem of whatever device you named. | | 🟡 | DELETE FROM table | Sometimes | — | Every row is affected. TRUNCATE also resets sequences and cannot be rolled back in MySQL. | | 🟡 | docker system prune -a --volumes | Sometimes | — | Removes stopped containers, unused images, networks and — with --volumes — named volumes and everything in them. | | 🟡 | DROP TABLE | Sometimes | — | The table and its data are removed. In PostgreSQL and MySQL this is not soft-deleted anywhere. | | 🟡 | emptied the Trash | Sometimes | — | The files are unlinked. macOS does not keep a second copy. | | 🟡 | gh repo delete | Sometimes | 90 days | The repository, its issues, PRs, releases, wiki and Actions history are deleted. | | 🟡 | git checkout -- file | Sometimes | — | Overwrites the file in the working tree with the version from the index. Uncommitted edits are discarded. | | 🟡 | git push --force | Sometimes | — | Replaces the remote branch history. Commits other people had pulled are orphaned on the server. | | 🟡 | git reset --hard | Sometimes | 90 days | Moves the branch pointer and overwrites the working tree and index to match. | | 🟡 | history -c | Sometimes | — | Clears the in-memory history list. On exit the shell may then write that empty list over the history file. | | 🟡 | kill -9 | Sometimes | — | SIGKILL cannot be caught, so the process gets no chance to flush buffers, write state or clean up locks. | | 🟡 | kubectl delete namespace | Sometimes | — | Cascades to every resource in the namespace, including PersistentVolumeClaims — which may delete the underlying disk depending on the reclaim policy. | | 🟡 | npm publish | Sometimes | 72 hours | Publishes to the public registry, permanently. Secrets in the tarball are now public and must be treated as compromised. | | 🟡 | rm -rf | Sometimes | — | Unlinks files immediately. There is no trash, no confirmation, and no undelete on APFS, ext4 or NTFS via this path. | | 🟡 | terraform destroy | Sometimes | — | Deletes every resource in the state file, in dependency order, in the real cloud account. | | 🟢 | force quit with unsaved work | Yes | — | The process dies without running its save-on-exit path. | | 🟢 | git branch -D | Yes | 30 days | Deletes the branch label even if it is unmerged. The commits become unreachable. | | 🟢 | git commit --amend | Yes | 90 days | Replaces the previous commit with a new one. The original is orphaned. | | 🟢 | git rebase on a shared branch | Yes | 90 days | Every commit gets a new SHA. Anyone who had the old commits now has a diverged history, and merging the two duplicates every commit. | | 🟢 | git stash drop | Yes | 30 days | Removes the stash entry. git stash clear removes all of them at once with no confirmation. |

You have is the window before recovery stops working — a git reflog expiry, an unpublish deadline, a soft-delete retention period. means there is no clock, which is either very good news or very bad news depending on the row.

Git

committed a secret

🔴 No — Gone. Recovery means a backup or nothing.

What it does: The secret is in git history, in every clone, and if pushed to a public repo it is being scraped within minutes.

Also: pushed .env · committed a private key · hardcoded API key

Recovery

Rotate the secret first. Everything else is secondary and slower than the scrapers. Public GitHub is scanned continuously; assume the key is used.

  1. Revoke and reissue the credential at its source.

  2. Then clean the history, if it is worth doing:

    # modern, fast, and what the git docs now recommend
    git filter-repo --invert-paths --path .env
    # or, for a value rather than a file
    git filter-repo --replace-text <(echo 'sk-1234==>REDACTED')
  3. Force-push all branches and tags, and tell every collaborator to re-clone — their old clones still contain it.

  4. On GitHub, old commits stay reachable by SHA even after a force push. Ask support to purge the cached views, or accept that it is exposed.

Next time

git-secrets or gitleaks as a pre-commit hook. Add .env to a global gitignore so it is never repo-specific. Use the platform's secret storage rather than files, and enable push protection on the repository.

⬆ back to the table

git branch -D

🟢 Yes — Reliably recoverable if you act. You have about 30 days.

What it does: Deletes the branch label even if it is unmerged. The commits become unreachable.

Also: git branch --delete --force

Recovery

git reflog # the branch tip is in here git checkout -b

he reflog entry is gone:

git fsck --unreachable --no-reflogs | grep commit git log --oneline # identify the right one git branch

achable objects are pruned by git gc after 30 days, or immediately if one ran git gc --prune=now.

Next time

Use git branch -d (lowercase), which refuses to delete unmerged work.

⬆ back to the table

git checkout -- file

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Overwrites the file in the working tree with the version from the index. Uncommitted edits are discarded.

Also: git restore file · git checkout .

Recovery

If the change was ever staged with git add, the blob is in the object database and recoverable:

git fsck --lost-found
ls .git/lost-found/other/
# or, if you know roughly when:
git stash list && git log -g --all

If it was never staged, git never saw it. Fall back to your editor's local history — VS Code Timeline, JetBrains Local History — which is the thing that actually saves people here.

Next time

git add early and often, even on work you are not ready to commit. Staging is free and makes everything recoverable. git stash before experiments.

⬆ back to the table

git clean -fd

🔴 No — Gone. Recovery means a backup or nothing.

What it does: Deletes untracked files and directories. With -x, ignored files too, which includes .env.

Also: git clean -fdx · git clean -xdf

Recovery

Git never knew about these files, so git cannot help. In order of hope:

  1. Editor local history — the only thing that regularly saves people:
    • VS Code: Timeline view in the Explorer, per file
    • JetBrains: right-click the folder → Local HistoryShow History
  2. Time Machine / File History / Dropbox / iCloud version history, if the directory was covered.
  3. .env specifically: check .env.example, your password manager, the deployment platform's environment variable page, or a teammate.

If none of those apply, it is gone. This is one of the few genuinely unrecoverable git commands.

Next time

git clean -nd first — -n is a dry run and lists exactly what would go. Make it muscle memory: never type -f before you have typed -n.

⬆ back to the table

git commit --amend

🟢 Yes — Reliably recoverable if you act. You have about 90 days.

What it does: Replaces the previous commit with a new one. The original is orphaned.

Also: git rebase -i · git rebase --squash

Recovery

git reflog git reset --hard HEAD@{1} # the commit before the amend

answer for a rebase that went wrong. ORIG_HEAD is set automatically by se and merge, so this usually works too:

git reset --hard ORIG_HEAD

Next time

Tag before a risky rebase — git tag pre-rebase costs nothing and reads clearly in a week.

⬆ back to the table

git push --force

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Replaces the remote branch history. Commits other people had pulled are orphaned on the server.

Also: git push -f · git push --force-with-lease

Recovery

  1. Do not let anyone else push. Every second the old history stays reachable somewhere is a second you can still recover it.

  2. Anyone who has not pulled yet is your backup. Ask the team. Someone's laptop almost certainly still has the old commits:

    git reflog                     # on their machine
    git push origin <sha>:refs/heads/rescue
  3. On GitHub, force-pushed commits stay reachable by SHA for a while even though they are on no branch. If you know the SHA — it is in the push event payload, the PR timeline, or any CI run log:

    https://github.com/OWNER/REPO/commit/<sha>
    git fetch origin <sha>
  4. GitHub's Events API still lists the old head:

    gh api repos/OWNER/REPO/events --jq '.[] | select(.type=="PushEvent") | .payload.before'

Next time

Use --force-with-lease always. It refuses when the remote moved since your last fetch, which is exactly the case where force-pushing hurts someone. Then protect the branch so the question stops arising.

⬆ back to the table

git rebase on a shared branch

🟢 Yes — Reliably recoverable if you act. You have about 90 days.

What it does: Every commit gets a new SHA. Anyone who had the old commits now has a diverged history, and merging the two duplicates every commit.

Also: git pull --rebase on main · rebasing after others pulled

Recovery

If nobody has pulled the rebased version yet, put it back:

git reflog
git reset --hard ORIG_HEAD           # rebase sets this automatically
git push --force-with-lease

If someone already pulled and merged, you now have duplicated commits. The fix is per-person, not central:

git fetch origin
git reset --hard origin/<branch>     # discard local, take the remote truth

Anyone with unpushed work should git cherry-pick it onto the new history rather than merging.

Next time

Only rebase branches nobody else has. The rule "rebase local, merge shared" exists precisely because this recovery is unpleasant for everyone at once.

⬆ back to the table

git reset --hard

🟡 Sometimes — Depends on what was configured beforehand. You have about 90 days.

What it does: Moves the branch pointer and overwrites the working tree and index to match.

Also: git reset --hard HEAD · git reset --hard origin/main

Recovery

Committed work is safe — the commits still exist, only the branch label moved.

git reflog                       # find the SHA from before the reset
git reset --hard <sha>           # or: git branch rescue <sha>

Uncommitted changes are a different story. If they were never staged, they are gone. If they were EVER staged (git add), the blobs are still in the object database:

git fsck --lost-found
ls .git/lost-found/other/        # your blobs, unnamed, one file each

Reflog entries expire after 90 days by default (30 for unreachable ones), so this window is real but generous.

Next time

git stash before any reset you are unsure about — it costs two seconds and makes the operation reversible. git reset --keep refuses to run instead of destroying uncommitted work.

⬆ back to the table

git stash drop

🟢 Yes — Reliably recoverable if you act. You have about 30 days.

What it does: Removes the stash entry. git stash clear removes all of them at once with no confirmation.

Also: git stash clear · git stash pop with conflict

Recovery

Stashes are commits, so they survive as unreachable objects:

git fsck --unreachable | grep commit
git show <sha>                          # find the right one
git stash apply <sha>

Faster if you only just did it — the SHA is printed in the output of git stash drop itself. Scroll up before you close the terminal.

Next time

Prefer git stash apply over git stash pop. apply keeps the stash entry after applying it; pop deletes it, including when the apply half-fails on a conflict and leaves you in a mess.

⬆ back to the table

Shell & filesystem

> file

🟡 Sometimes — Depends on what was configured beforehand.

What it does: The shell truncates the target to zero bytes before the command runs — so a typo destroys the file even if the command then fails.

Also: echo > file · : > file · command > existing-file

Recovery

If any process still holds the file open, the data is still on disk under the old inode. On Linux this is a genuine rescue:

lsof <file>
cp /proc/<pid>/fd/<n> <file>.recovered

A tailing log, an editor, or the server itself will often be that process. Once every handle closes, the data is unlinked and gone.

Otherwise: backups, version control, editor local history.

Next time

set -o noclobber makes > refuse to overwrite an existing file (use >| when you mean it). Prefer >> when appending is what you actually want.

⬆ back to the table

chmod -R 777

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Overwrites every permission bit in the tree. The previous permissions are not recorded anywhere.

Also: chmod -R · chown -R · chmod 777 /

Recovery

Nothing stores the old modes, so you rebuild them:

  • In a git repo: git tracks only the executable bit, but that is usually the one that matters — git diff will show mode changes, and git checkout . restores them.
  • System directories on macOS: Disk Utility → First Aid repairs permissions on system paths only.
  • Linux packages: reinstall to restore package-owned modes — dpkg --verify or rpm -Va lists exactly what changed first.
  • chown -R on /: this typically requires a reinstall or a restore. Boot from external media and copy user data off first.

A sane default for a project tree: find . -type d -exec chmod 755 {} + and find . -type f -exec chmod 644 {} +, then fix executables individually.

Next time

Capture the state first — getfacl -R . > perms.txt (restore with setfacl --restore=perms.txt). And 777 is essentially never the right answer; it is a way of not diagnosing a permissions problem.

⬆ back to the table

crontab -r

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Deletes your entire crontab. There is no confirmation prompt, and -r sits directly next to -e on the keyboard.

Also: crontab -ri

Recovery

  1. Backups of the spool file — this is where it usually is:

    /var/spool/cron/crontabs/<user>      # Debian/Ubuntu
    /var/spool/cron/<user>               # RHEL/CentOS
    /usr/lib/cron/tabs/<user>            # macOS

    Restore that path from Time Machine, restic, or a filesystem snapshot.

  2. Mail: cron emails output to the user. Old mail in /var/mail/<user> often reveals what was running and how often.

  3. Syslog: grep CRON /var/log/syslog* lists every command cron ran, with times. You can reconstruct the schedule from the pattern.

Next time

alias crontab='crontab -i' prompts before destroying. Better: keep the crontab in version control and install it with crontab crontab.txt, so the file on disk is never the only copy.

⬆ back to the table

dd of=/dev/disk

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Writes raw bytes over the partition table and filesystem of whatever device you named.

Also: dd if=image.iso of=/dev/sda · Etcher to the wrong drive

Recovery

Unmount the disk and stop immediately. Do not let anything else write.

  1. If only the partition table was overwritten — likely, if you stopped early — the filesystems are still there and can be found:

    testdisk /dev/sdX          # scans for lost partitions, rewrites the table
  2. Filesystem-level carving for individual files: photorec. It ignores the partition table entirely and recovers by file signature. Write the output to a different disk.

  3. Overwritten regions are gone. dd is a byte-for-byte write; there is no journal and no shadow copy.

Next time

Say the device name out loud before pressing enter. lsblk immediately before, every time. Prefer tooling that refuses system disks — and note that /dev/disk2 on macOS is not the same disk after a reboot.

⬆ back to the table

history -c

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Clears the in-memory history list. On exit the shell may then write that empty list over the history file.

Also: rm ~/.bash_history · rm ~/.zsh_history

Recovery

Do not close this terminal. Recovery depends on it staying open.

  1. The on-disk file is usually still intact until the shell exits and overwrites it. Copy it now, from a different terminal:

    cp ~/.zsh_history ~/zsh_history.backup
  2. Then stop the current shell from writing over it:

    unset HISTFILE          # in the affected shell, before exiting
  3. If the file is already gone: Time Machine, or lsof | grep history in case a process still holds a handle to it.

Next time

setopt INC_APPEND_HISTORY in zsh (or PROMPT_COMMAND='history -a' in bash) writes each command immediately instead of at exit, which makes the file authoritative and this whole failure mode disappear.

⬆ back to the table

kill -9

🟡 Sometimes — Depends on what was configured beforehand.

What it does: SIGKILL cannot be caught, so the process gets no chance to flush buffers, write state or clean up locks.

Also: killall -9 · pkill -9 · Force Quit

Recovery

The process is gone; the question is what it left behind.

  • A database killed mid-write usually recovers on next start via its write-ahead log. Start it and read the logs before doing anything clever.
  • A stale lock file is the common aftermath — postmaster.pid, .lock, LOCK. Remove it only after confirming with ps that no process is actually running.
  • Unflushed application buffers are lost. Anything that had not hit disk was never written.
  • Half-written files: check size and tail before trusting them.

Next time

Try kill (SIGTERM) and wait ten seconds first — well-behaved processes shut down cleanly. kill -9 should be the second attempt, never the first.

⬆ back to the table

rm -rf

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Unlinks files immediately. There is no trash, no confirmation, and no undelete on APFS, ext4 or NTFS via this path.

Also: rm -r · rm -f · unlink

Recovery

Stop writing to that disk now. Every write reduces your chances. If it is the system disk, that means stop working on the machine.

In order of likelihood:

  1. A process still has the file open — this is the surprise recovery and it works more often than people expect. On Linux:

    lsof | grep deleted
    cp /proc/<pid>/fd/<n> /tmp/recovered
  2. Backups: Time Machine (tmutil), restic/borg, Dropbox or iCloud version history, your editor's local history, a Docker volume, a node_modules copy, a CI artifact.

  3. Git: if it was ever committed, git checkout . brings it back.

  4. Undelete tools — only worth trying on a spinning disk or a non-TRIM drive. On modern SSDs, TRIM has usually already zeroed the blocks: photorec, testdisk, extundelete.

Next time

Install trash-cli and alias rm to it, so deletes are reversible by default. Never build a path by variable interpolation without a guard — rm -rf "$DIR/" deletes / when $DIR is unset. set -u in every script.

⬆ back to the table

Databases

DELETE FROM table

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Every row is affected. TRUNCATE also resets sequences and cannot be rolled back in MySQL.

Also: UPDATE without WHERE · TRUNCATE TABLE

Recovery

  1. Have you committed? If not:

    ROLLBACK;

    This is the single most common successful recovery, and the reason to run BEGIN; before anything destructive.

  2. Committed, PostgreSQL: point-in-time recovery from WAL. Deleted rows technically remain in the heap until vacuumed, but there is no supported way to read them — do not pin your hopes on pg_dirtyread.

  3. Committed, MySQL with binlog: replay to just before the statement.

    mysqlbinlog --stop-datetime="2026-08-15 14:31:59" binlog.000123 | mysql
  4. Managed databases (RDS, Cloud SQL, PlanetScale, Neon): console → restore to point in time. Neon branches make this close to instant.

Next time

Write the WHERE clause before the DELETE. Run it as a SELECT first and read the row count. Use a client with a "safe updates" mode — SET sql_safe_updates = 1 in MySQL rejects an unqualified DELETE outright.

⬆ back to the table

DROP TABLE

🟡 Sometimes — Depends on what was configured beforehand.

What it does: The table and its data are removed. In PostgreSQL and MySQL this is not soft-deleted anywhere.

Also: DROP DATABASE · DROP SCHEMA

Recovery

  1. If you are still inside a transaction, stop and think, then:

    ROLLBACK;

    PostgreSQL has transactional DDL, so an uncommitted DROP TABLE rolls back completely. MySQL does not — DDL commits implicitly there.

  2. Point-in-time recovery, if WAL/binlog archiving is on. This is the real answer for production:

    # Postgres: restore base backup, replay WAL to just before the drop
    recovery_target_time = '2026-08-15 14:32:00'
    
    # RDS / Cloud SQL / Aurora: console → Restore to point in time

    PITR restores to a new instance. You then copy the one table back.

  3. The most recent logical dumppg_restore -t <table> can pull a single table out of a full dump without touching anything else.

Next time

Revoke DDL from the application role. Require a transaction and an explicit count first: BEGIN; SELECT count(*) FROM t; — if the number surprises you, you are on the wrong database. Set \set AUTOCOMMIT off in .psqlrc.

⬆ back to the table

Docker

docker system prune -a --volumes

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Removes stopped containers, unused images, networks and — with --volumes — named volumes and everything in them.

Also: docker volume rm · docker volume prune · docker rmi

Recovery

  • Images: re-pull them. Annoying, not fatal, unless the tag was rebuilt or deleted upstream. Check whether the digest still exists.
  • Containers: recreated from your compose file. Anything written inside a container and not in a volume was already ephemeral.
  • Named volumes: this is the real loss. Databases live here. On Linux the data was under /var/lib/docker/volumes/<name>/_data and is now unlinked — filesystem-level undelete is the only route, and on an SSD with TRIM it is almost certainly gone.
  • Docker Desktop: everything lives in one big VM disk image, so per-file recovery is not realistic.

Restore from your database backup. If you do not have one, this is the moment you find out.

Next time

docker system prune without -a --volumes is much safer and does what people usually mean. Bind-mount anything you care about to a host path that your normal backups already cover, instead of trusting a named volume.

⬆ back to the table

Kubernetes

kubectl delete namespace

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Cascades to every resource in the namespace, including PersistentVolumeClaims — which may delete the underlying disk depending on the reclaim policy.

Also: kubectl delete pvc · kubectl delete -f · helm uninstall

Recovery

  1. Check the PV reclaim policy immediately. If it was Retain, the underlying disk still exists and is merely Released:

    kubectl get pv | grep Released
    # edit the PV, clear .spec.claimRef, then re-create the PVC to bind it

    If it was Delete, the cloud disk is being deleted right now. Go to the cloud console and look for a snapshot.

  2. Velero or similar: velero restore create --from-backup <name>.

  3. etcd snapshot: restores cluster state, but not data on deleted disks.

  4. GitOps: if the manifests are in git, re-applying restores the configuration in minutes. It does not restore data.

Next time

Set persistentVolumeReclaimPolicy: Retain on anything stateful. Never point kubectl at a cluster without checking kubectl config current-context first — put it in your shell prompt. Use kubectl delete --dry-run=server.

⬆ back to the table

AWS

aws ec2 terminate-instances

🟡 Sometimes — Depends on what was configured beforehand.

What it does: The instance is destroyed. Root EBS volumes with DeleteOnTermination=true go with it; other attached volumes usually survive.

Also: terraform destroy · gcloud compute instances delete

Recovery

  1. Check for snapshots first — this is the whole game:

    aws ec2 describe-snapshots --owner-ids self \
      --filters Name=volume-id,Values=vol-xxxx

    A snapshot means you can create a new volume and attach it.

  2. Non-root volumes with DeleteOnTermination=false are still there, available, waiting to be attached to a new instance.

  3. AWS Backup / Data Lifecycle Manager recovery points.

  4. The instance itself cannot be un-terminated. You rebuild it — which is fine if it was in Terraform or an AMI, and painful if it was a pet.

Next time

Enable termination protection on anything long-lived. Set DeleteOnTermination=false on data volumes. Above all: if you cannot recreate an instance from code, the instance is a liability regardless of whether anyone deletes it.

⬆ back to the table

aws s3 rm --recursive

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Deletes objects. With versioning on, it writes delete markers instead of destroying data.

Also: aws s3 rb --force · s3 delete-object

Recovery

If versioning was enabled, you have lost nothing. The objects are hidden behind delete markers:

aws s3api list-object-versions --bucket B --prefix P \
  --query 'DeleteMarkers[].{Key:Key,Id:VersionId}' --output json > markers.json

# delete the delete markers to reveal the objects again
aws s3api delete-object --bucket B --key K --version-id <marker-id>

If versioning was off, the objects are gone. Check for:

  • A replication target bucket in another region
  • AWS Backup, if the bucket was in a backup plan
  • Glacier / lifecycle-transitioned copies

A deleted bucket name is released and can be re-created after a delay, but the data does not come back with it.

Next time

Turn on versioning plus MFA delete on anything that matters, and add a lifecycle rule to expire old versions so it does not cost a fortune. Use --dryrun — the AWS CLI supports it on every s3 rm — and prefer bucket policies that deny s3:DeleteObject outright for application roles.

⬆ back to the table

Terraform

terraform destroy

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Deletes every resource in the state file, in dependency order, in the real cloud account.

Also: terraform apply with a destructive plan · terraform state rm

Recovery

Stop the run now if it is still going — Ctrl-C once, and Terraform finishes the current resource and stops. That can save the rest.

Then, per resource type:

  • Databases: RDS may have a final snapshot if skip_final_snapshot = false. Restore from it.
  • S3: covered by versioning (see the aws s3 rm entry).
  • Anything with soft-delete: Azure Key Vault, GCS soft delete, Route53 zones can be re-created but with new nameservers.
  • Everything else: terraform apply recreates the infrastructure from the same code. New IDs, new IPs, empty disks.

terraform state rm is the recoverable one — it only forgets a resource, it does not delete it. Re-import with terraform import.

Next time

prevent_destroy = true in a lifecycle block on anything stateful. Never run destroy without reading the plan count. Separate state files so that production cannot be destroyed by a command aimed at staging.

⬆ back to the table

GitHub

gh repo delete

🟡 Sometimes — Depends on what was configured beforehand. You have about 90 days.

What it does: The repository, its issues, PRs, releases, wiki and Actions history are deleted.

Also: Settings → Delete this repository · gh repo archive

Recovery

  1. Ask GitHub Support to restore it. Repositories are recoverable for roughly 90 days after deletion, and support does this routinely: https://support.github.com/contact — give them the exact owner/repo. Do not create a new repo with the same name first; that blocks the restore.
  2. The code itself is in every clone anyone has. git push it to a new remote and you have lost only the metadata.
  3. Issues and PRs are the part you cannot rebuild from a clone. If you have any export, or a mirror on another forge, use it.

Next time

Archive instead of deleting — it is reversible and makes the repo read-only. Keep a mirror: git clone --mirror on a cron job to any second location.

⬆ back to the table

Package registries

npm publish

🟡 Sometimes — Depends on what was configured beforehand. You have about 72 hours.

What it does: Publishes to the public registry, permanently. Secrets in the tarball are now public and must be treated as compromised.

Also: npm publish --access public

Recovery

If it contains a secret, rotate the secret first. Unpublishing does not un-copy it — mirrors, caches and scrapers act within minutes. Assume it is public forever and revoke the key.

Then, within 72 hours of publishing, you may unpublish:

npm unpublish <pkg>@<version>

After 72 hours npm only allows it if the package has no dependents and few downloads. The usual path instead is:

npm deprecate <pkg>@<version> "Published in error, use 1.2.4"
npm publish        # a new, higher version that is correct

The version number is burned either way — npm never lets you reuse it.

Next time

npm publish --dry-run and read the file list. Use the files field in package.json as an allowlist rather than .npmignore as a denylist — a forgotten deny rule ships your .env, a missing allow rule just omits a file.

⬆ back to the table

Editors & unsaved work

force quit with unsaved work

🟢 Yes — Reliably recoverable if you act.

What it does: The process dies without running its save-on-exit path.

Also: kill -9 editor · terminal closed · laptop died

Recovery

Most editors write recovery files continuously. Look before you retype:

  • Vim: a swap file sits next to the original. Reopen the file and Vim offers Recover, or do it explicitly:

    vim -r file.txt
    ls -a .file.txt.swp
  • VS Code: reopening usually restores unsaved buffers automatically. If not, the backups are on disk:

    ~/Library/Application Support/Code/Backups/     # macOS
    ~/.config/Code/Backups/                         # Linux
  • JetBrains: Local History on the file, which survives crashes.

  • Office / Google Docs: version history, autosave, "Recover unsaved".

  • A shell heredoc or long command: check ~/.bash_history, ~/.zsh_history, or your terminal's scrollback buffer before closing it.

Next time

Autosave on. It is a two-minute setting change that ends this category of loss permanently.

⬆ back to the table

macOS

emptied the Trash

🟡 Sometimes — Depends on what was configured beforehand.

What it does: The files are unlinked. macOS does not keep a second copy.

Also: Shift-Delete · Empty Bin · secure empty trash

Recovery

  1. Time Machine — enter it and browse to the folder and date. This is the recovery that works, and the reason to have it switched on.
  2. iCloud Drive: iCloud.com → Account Settings → Restore Files. It keeps deleted files for 30 days independently of the local Trash.
  3. Dropbox / Google Drive / OneDrive: all keep deleted files for 30 days or more, in a separate web trash that emptying the local Trash does not touch.
  4. Photos and Notes have their own "Recently Deleted" albums, also 30 days, also unaffected by the Finder Trash.
  5. Undelete software is a long shot on an APFS SSD with TRIM enabled.

Next time

Time Machine to any external disk, plus one offsite copy. It is the only entry on this list that covers all of the others.

⬆ back to the table

General

answered Yes to a destructive prompt

🟡 Sometimes — Depends on what was configured beforehand.

What it does: Varies. What matters is that you now need to know what it actually did.

Also: clicked through a warning · --yes · --force · -y

Recovery

Before trying to fix anything, find out what happened. Fixing the wrong thing makes recovery harder.

  1. Read the scrollback. Most destructive tools print what they removed. Copy it somewhere before the buffer scrolls away.
  2. Check the shell history for the exact command and its flags — history | tail -20.
  3. Check for logs: ~/.npm/_logs, /var/log/, journalctl --since "10 min ago", the cloud provider's audit trail (CloudTrail, Cloud Audit Logs, Azure Activity Log). Cloud audit logs are the single best source of truth for "what did I just delete" and almost nobody thinks to look.
  4. Then find the specific entry in this repo for that command.

Next time

Never pass -y to a command you have not run without it first.

⬆ back to the table


Contributing

One file in entries/. The bar is that you have actually done the recovery, or you cite someone who has. Plausible-sounding recovery steps are worse than none, because they get tried first and waste the window. See CONTRIBUTING.md.

Licence

Code MIT, content CC0.

Generated by scripts/build.mjs. Do not edit this file directly.