Rule: never create an atom from external or unbounded input at runtime. The BEAM atom table is capped (about 1,048,576 entries by default) and atoms are never garbage-collected. Turning CLI-, JSON-, environment-, registry-, or model-derived strings into atoms can exhaust the table and terminate the VM.
Banned on untrusted input
- runtime atom-conversion helpers from
String,List, or:erlang - interpolated atom syntax built from runtime values
Jason.decode!(json, keys: :atoms)and equivalent decoder options
Safe patterns
- Use an explicit static map or
casefrom known strings to literal atoms, with the original string as the fallback. Unknown wire values must remain strings. The app-server parameter normalizers andCodex.Eventsfollow this pattern. - Use an existing-atoms-only conversion only when the atom is provably predefined. A static lookup is usually clearer and avoids exceptions.
- Decode JSON with string keys.
keys: :atoms!is safe only when atom keys are genuinely required because it resolves existing atoms only. - Keep opaque values such as model IDs, event types, plugin keys, and tool names as strings unless the SDK has a finite, declared atom vocabulary.
Guardrails
Two independent checks run in mix ci:
Credo.Check.Warning.UnsafeToAtom, scoped tolib/in.credo.exs.scripts/atom_guard.sh, anrgbackstop that rejects dynamic-atom patterns inlib/**/*.exunless a reviewed same-line# atom-safe:annotation is present.
Bounded compile-time module-name construction may be annotated with both
# atom-safe: and credo:disable-for-next-line when Credo cannot infer that
the values come only from source declarations.