Skip to main content

Create self-cleaning ephemeral directories that vanish after a lifetime or on restart.

Project description

ephemdir

PyPI version Python versions License: MIT

ephemdir (ephemeral directory) creates temporary directories that clean themselves up — automatically removed once their lifetime expires or after the machine restarts. Names are playful two-word slugs with a random token, like brave-otter-a81f42c9d047315b, instead of dull ones like tmp_data — and you can address them by any unique prefix (brave or brave-otter).

Supports Python 3.10+ on Linux and macOS with safe fd-relative cleanup. Windows is intentionally unsupported until Python or a dedicated backend can provide handle-bound recursive deletion; tempdir() fails before creating anything on unsupported platforms. The package has no third-party runtime dependencies on Python 3.11+; Python 3.10 uses tomli only for TOML configuration parsing.

Installation

[!IMPORTANT] Windows is not supported in ephemdir 0.4.0. The package may install on Windows, but tempdir() and ephemdir new fail before creating anything because Python does not expose the safe handle-bound recursive deletion primitives ephemdir requires. Supported platforms are Linux and macOS.

macOS (Homebrew)

brew install vindfjur/tap/ephemdir

Or tap once and install by name:

brew tap vindfjur/tap
brew install ephemdir

Linux

For command-line use, pipx keeps ephemdir isolated from system packages:

pipx install ephemdir

Or install from PyPI with pip:

pip install ephemdir

Python package

Add ephemdir to your project dependencies, or install it into the current environment:

python -m pip install ephemdir

Quick start

from ephemdir import tempdir

# Lives until the next system restart (the default).
work = tempdir()
print(work)                       # -> /current/dir/brave-otter-a81f42c9d047315b
(work.path / "data.txt").write_text("hello")

# Also expires after a given lifetime.
cache = tempdir(lifetime="2h")    # "30m", "1h30m", 3600, timedelta(hours=2) ...

# Survive restarts and rely only on the lifetime.
keep = tempdir(lifetime="7d", remove_on_restart=False)

# Do not delete while a process still has files open inside (Linux/macOS).
busy = tempdir(lifetime="1h", keep_while_in_use=True)

# Use as a context manager: removed automatically when the block exits.
with tempdir() as scratch:
    (scratch / "tmp.bin").write_bytes(b"...")

tempdir() parameters

Parameter Default Description
lifetime None Time to live: seconds, timedelta, or "2h"/"1h30m". None = until restart.
remove_on_restart True Remove the directory after the machine reboots.
keep_while_in_use False Defer deletion while files are still open inside (Linux/macOS).
parent current dir Where to create the directory.
prefix "" Prefix prepended to the generated name.
words 2 Number of words in the generated name (a 64-bit random hex token is always appended).

Any option left unset falls back to the user config file, then to the built-in default shown above.

Managing a directory

d = tempdir()
d.path          # pathlib.Path to the directory
d.created_at    # creation timestamp
d.expires_at    # expiry timestamp or None
d.remove()      # delete now and stop tracking
d.keep()        # stop tracking but keep on disk (becomes permanent)
d.extend("2h")  # fresh lifetime counted from now (None = no time limit)

You can also manage any tracked directory later — by full path, exact name or a unique prefix of the name:

from ephemdir import keep, extend, remove, resolve, prune

keep("brave-otter")          # liked it? make it permanent
extend("brave-otter", "2h")  # give it two more hours from now
remove("brave-otter")        # delete it right away
resolve("bra")               # -> Path to the matching tracked directory
prune()                      # forget entries whose dirs were deleted manually

How cleanup works

ephemdir records each directory in a small JSON registry in your per-user data directory. Cleanup happens in two ways:

  1. Lazily — every call to tempdir() first sweeps anything already due.
  2. On demand — run ephemdir sweep from the command line.

Safety guarantees

ephemdir never auto-deletes a directory it cannot prove it created:

  • Each directory contains a hidden .ephemdir marker file with a random id that is also stored in the registry (plus the inode on Unix). Nothing is auto-deleted unless the marker matches — if you delete a directory manually and something else later appears at the same path, ephemdir leaves it alone (shown as ⚠️ replaced in ephemdir list). Directories registered by ephemdir ≤ 0.3 have no marker, so they are never auto-removed either: they show as ⚪ legacy until you ephemdir rm or ephemdir keep them.
  • Deletion is journaled and claimed under locks: the registry records the intent durably before the directory is renamed away and commits afterwards, every step re-verifies the entry and the marker, and each directory has its own OS-level deletion lock. A concurrent keep or extend always wins against a running sweep (keep() inside a with tempdir() block is honoured), two overlapping sweeps never delete the same tree twice, and a crash at any point is reconciled by the next sweep's recovery pass under the same deletion lock. A half-deleted tree stays at its private staging path and is retried; it is never renamed over a new object at the original path. Ambiguous cases are parked as recovery and can be handled with ephemdir recover. Nothing mid-deletion becomes untracked.
  • On POSIX, recursive staging deletion is bound to the verified directory object: ephemdir opens the staging tree with O_DIRECTORY | O_NOFOLLOW, checks fstat() against the claimed inode/marker and removes contents via dir_fd operations. If the staging pathname is replaced after verification, the replacement is left alone and the entry moves to recovery.
  • Linux cleanup refuses to cross mount boundaries by comparing mount ids from opened directory fds. It uses statx(STATX_MNT_ID) when available and falls back to /proc/self/fdinfo or /proc/self/mountinfo on older kernels. Mount capability is checked before claim, then rechecked before recursion.
  • Directory creation and claim/delete require a trusted parent: owner-only parents are accepted, and shared writable parents must have sticky bit set. Group/world-writable parents without sticky bit are refused before rename.
  • Platforms without safe fd-bound recursive deletion do not fall back to pathname-based rmtree. tempdir() refuses to create new managed directories, and cleanup capability is rechecked before claim so existing entries remain untouched.
  • A deletion guard refuses to touch the filesystem root, your home directory or ephemdir's own data — even with sweep --force and even if the registry file was corrupted or edited by hand. Malformed registry entries are rejected on load and a corrupt registry is quarantined, not overwritten.
  • Directories are created owner-only (0700), like tempfile.mkdtemp.
  • Optional lsof and macOS sysctl probes ignore inherited PATH, resolve helpers only from trusted system directories and run with a minimal environment. If keep_while_in_use is enabled and lsof cannot run, the sweep keeps the directory and warns instead of deleting.
  • Generated names end with a 64-bit random token, so another local user cannot pre-create the whole name space in a shared sticky directory like /tmp and block creation.
  • The scheduled service runs python -I -m ephemdir from / with a scrubbed environment, and install-service verifies both that isolated mode resolves ephemdir to the installed package and that every directory on the way to the systemd/launchd unit files is owned and writable only by you (service files are then written dir_fd-relative to the verified directory).
  • The config file is honoured only when it is yours: a config.toml owned by another user or writable by group/others is ignored with a warning.

When an interrupted deletion is genuinely ambiguous, inspect it with ephemdir list --json. After resolving the filesystem situation, retry it with ephemdir recover <name>. ephemdir recover <name> --forget removes only the registry entry and never deletes either the original or staging path.

To clean up reliably over time (and right after a reboot), install a scheduled sweep service for your platform — one command, no manual unit files:

ephemdir install-service --interval 600   # launchd / systemd
ephemdir uninstall-service

install-service validates the persistent runtime before writing anything: every component of the interpreter and package paths must be owned by you (or root) and must not be writable by other users. This deliberately rejects any group/world-writable component — including sticky directories like /tmp — so a virtualenv created under /tmp cannot host the scheduled service even though it is fine for one-off interactive use. Install the venv under your home directory instead. It also checks the interpreter-startup hooks Python runs before ephemdir is imported (.pth files in site-packages, sitecustomize, pyvenv.cfg, and the tomli package on Python 3.10).

Trust boundary for install-service. The scheduled job runs your Python interpreter unattended and later, as your user. ephemdir verifies its own package, the interpreter, and the startup hooks above, but it cannot exhaustively vet every module a .pth file or a runtime dependency might import — that is the same surface as any Python command you run. Only install the service from a Python environment whose site-packages are not writable by other local users (a normal per-user pip/venv install satisfies this). On a single-user machine there is nothing extra to do; on a shared multi-user host, ensure the environment is owned by you and not group/world-writable before scheduling sweeps.

Prefer to wire it up yourself? The equivalents are:

  • Linux (cron): */10 * * * * ephemdir sweep
  • macOS (launchd): a LaunchAgent running ephemdir sweep; template in packaging/.

You can also keep a foreground watcher running:

ephemdir watch --interval 600

Configuration

Set per-user defaults in a config.toml file in your config directory (~/.config/ephemdir/ on Linux or ~/Library/Application Support/ephemdir/ on macOS). Every key is optional:

lifetime = "6h"
remove_on_restart = true
keep_while_in_use = true
prefix = "scratch-"
words = 2

These apply to any option you do not pass explicitly to tempdir() or the ephemdir new command. Reading the file uses the standard-library tomllib (Python 3.11+) or tomli on older versions.

The environment variables EPHEMDIR_DATA_DIR (registry location) and EPHEMDIR_CONFIG_DIR (config location) override the platform defaults — handy for sandboxes, tests and dotfile-managed setups.

Command-line interface

ephemdir new                     # create a directory, print its path
ephemdir new --lifetime 2h       # with a lifetime
ephemdir new --keep-on-restart   # do not remove on restart
ephemdir list                    # tracked directories, status and time left
ephemdir path brave-otter        # print a tracked directory's path
ephemdir keep brave-otter        # liked it? stop tracking, keep forever
ephemdir extend brave-otter 2h   # fresh lifetime from now (--forever: no limit)
ephemdir rm brave-otter          # remove a tracked directory now
ephemdir sweep                   # remove everything due now
ephemdir sweep --force           # remove every tracked directory
ephemdir prune                   # forget entries deleted outside ephemdir
ephemdir recover brave-otter     # retry an interrupted deletion
ephemdir recover brave-otter --forget  # forget it without deleting files
ephemdir watch                   # sweep periodically in the foreground
ephemdir install-service         # schedule sweeps via the OS scheduler
ephemdir uninstall-service       # remove the scheduled service

Every command that takes a directory accepts a full path, the exact name or a unique prefix (bra or brave-otter for brave-otter-a81f42c9d047315b). Add -v for more output or -q to stay quiet.

Listing with time left

ephemdir list shows each directory's status at a glance:

🟢 spotted-armadillo   1h 59m left              ~/work/spotted-armadillo
🟡 vermilion-mackerel  4m 59s left              ~/work/vermilion-mackerel
🔄 caped-dodo          until restart            ~/work/caped-dodo
🔴 furious-caiman      expired 5m ago           ~/work/furious-caiman
👻 lucky-yak           gone (deleted manually)  ~/work/lucky-yak

🟢 counting down · 🟡 less than 15 minutes left · 🔴 due on the next sweep · 🔄 until restart · 📌 no auto-cleanup · 👻 deleted outside ephemdir (the entry is dropped automatically) · ⚠️ replaced by another directory (never touched) · legacy entry from ephemdir ≤ 0.3 (resolve with rm/keep) · 🚧 interrupted deletion requiring recover · temporarily inaccessible but still tracked. Terminals without emoji support fall back to ASCII tags; --plain forces them, --json prints machine-readable output for scripting.

Jumping into directories (ecd / enew)

A subprocess cannot change your shell's working directory, so ephemdir ships shell functions instead (the same trick zoxide and nvm use). Add one line to your shell rc file:

# ~/.bashrc or ~/.zshrc
eval "$(ephemdir shell-init)"

# ~/.config/fish/config.fish
ephemdir shell-init fish | source

Then:

enew -l 2h        # create a directory and cd into it
ecd brave-otter   # cd into a tracked directory by name or prefix
ecd               # cd into the most recently created one

The new and path commands print the path to stdout and all diagnostics to stderr, so they also compose cleanly by hand: cd "$(ephemdir new)".

Development

pip install -e ".[dev]"
pytest

Reproducible source builds

pyproject.toml only declares the minimum safe build backend (setuptools>=78.1.1, which includes the fix for CVE-2025-47273), so a plain isolated build resolves whatever backend version is current. To build exactly what release CI builds, use the hash-pinned toolchain:

python -m pip install --require-hashes -r requirements-build.txt
python -m build --no-isolation

Release artifacts are produced this way in .github/workflows/publish.yml; the workflow records SHA-256 hashes of dist/ right after the build and verifies them again after the install smoke test.

License

MIT © vindfjur

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ephemdir-0.4.0.tar.gz (99.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ephemdir-0.4.0-py3-none-any.whl (65.6 kB view details)

Uploaded Python 3

File details

Details for the file ephemdir-0.4.0.tar.gz.

File metadata

  • Download URL: ephemdir-0.4.0.tar.gz
  • Upload date:
  • Size: 99.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for ephemdir-0.4.0.tar.gz
Algorithm Hash digest
SHA256 16af129720bfe017c501a0718963b974ee071dbcea02fcc0bf69c8600d145b16
MD5 1f772d6dcacccf91ec9b9eb79074c5d0
BLAKE2b-256 0d65b391ff345d6e931c7dd48264d18858cda73e78f3d2279f70da961f1180eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for ephemdir-0.4.0.tar.gz:

Publisher: publish.yml on vindfjur/ephemdir

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ephemdir-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: ephemdir-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 65.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for ephemdir-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e85c2638680227420e26353fe84c73aa24d5c8d98b1bd6f098de7d23a9125929
MD5 dd9b82bda0a3459a60780271e2112863
BLAKE2b-256 7cb103905b11a02fa38d651e3be1f8c57aa17cb6d25fb5d8e6aec16a180306da

See more details on using hashes here.

Provenance

The following attestation bundles were made for ephemdir-0.4.0-py3-none-any.whl:

Publisher: publish.yml on vindfjur/ephemdir

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page