Adapted from Wes Jones’ The Ideal Way to Name Your Files, with adjustments for code/config contexts.
The single most useful insight is that hyphens and underscores aren’t interchangeable: most tools (search engines, IDEs, editors with “match whole word”) treat - as a word separator and _ as a word joiner. That mechanical difference is the whole game — which one you want depends on whether you want the parts of the name to be individually findable or treated as one atomic token.
That gives us two regimes.
Use hyphens for: assets, documents, URLs, public-facing files
This is Wes’s domain — photo/video assets, marketing files, anything that ends up indexed by a search engine or sorted in a finder window. Here you want the parts of the name to be discoverable on their own, because you’ll search for “bmw” or “studio” or “fall-campaign” without knowing the rest.
The convention: YYMMDD-BRAND-PROJECT-HARDWARE-NNN-vN.ext
- Descending dates first (
240614, not06-14-2024) so chronological sort is free. Two-digit year, zero-padded month and day, all the same width. - Brand code — short, 2–4 chars (
BMW,TNF,YETI). - Project code — short and descriptive (
studio,fall-campaign). Keep the whole filename under ~25 chars where you can. - Hardware code — only when it matters (multi-camera shoots, separate audio).
R51,R52,F3. - Asset number — zero-padded to the max width you’ll need (
001, not1, if you’ll have hundreds). Assigned at import time and never re-numbered on partial export. - Version —
v1,v2, appended after the asset number so the rest of the system doesn’t shift. - No spaces, no special characters (
" / \ [ ] : ; | = , < ? > & $ # ! ' { } ( ) *). Letters, numbers, and hyphens only.
Use underscores for: code, scripts, configs, anything searched by exact identifier
This is the case my Postgres-upgrade work surfaced. In a codebase you frequently search for an exact filename — upgrade.sh, config.py, auth_handler.go — and you want that search to land only on real matches.
With hyphens, pg-upgrade.sh matches a whole-word search for upgrade.sh (because - is a separator, so upgrade and sh are seen as standalone words). With underscores, pg_upgrade.sh is one token and the search for upgrade.sh correctly ignores it.

So in code-adjacent contexts:
- Underscores between words in filenames:
pg_upgrade.sh,test_upgrade_chain.sh,pg_version_upgrade.sh. Each filename is one atomic identifier. - Hyphens only for genuine separators between distinct conceptual fields, the same role they play in the asset convention above. e.g.
pg_upgrade-v2.shor2024-01-config_backup.json— hyphen separates fields, underscore joins words within a field. - Same restrictions otherwise — no spaces, no special characters, lowercase.
- Match the language’s conventions where they exist. Python modules want
snake_case.py. Go files wantlower_snake.goorlowercase.go. JavaScript/TypeScript is mixed butkebab-case.tsis common — and in JS land that’s usually fine because you rarely whole-word-search bare filenames the way you do in a polyglot shell-script repo.
What’s shared across both
The underlying discipline is the same regardless of regime:
- Descending dates (
YYMMDDorYYYY-MM-DD) when dates are part of the name. ISO-ish ordering means filesystem sort = chronological sort, for free. - Zero-pad numbers to a consistent width.
- Be brief. Every part of the filename contributes context — you don’t need to encode everything in every field.
- Be consistent. The convention only pays off if it’s applied uniformly; one rogue
Upgrade Script (final FINAL).shundoes the whole system for that directory.
TL;DR
| Context | Separator within filename | Why |
|---|---|---|
| Photo/video/document assets, URLs, anything you’ll fuzzy-search | - (hyphen) |
Hyphens are word separators — parts of the name are individually findable |
| Code, scripts, configs, anything you’ll exact-search | _ (underscore) |
Underscores are word joiners — the filename is one atomic identifier and upgrade.sh won’t false-match in pg_upgrade.sh |
| Field separators in either regime | - (hyphen) |
Reserve hyphens for boundaries between distinct conceptual fields (date / brand / project / version) |