code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when Elixir/Phoenix code handles params or other user input, String.to_atom on external strings, tokens, redirects, raw SQL, deserialization, outbound HTTP or security headers, or when asked to review code for security — injection, XSS, atom exhaustion, SSRF, open redirects,
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill security-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/security-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when Elixir/Phoenix code handles params or other user input, String.to_atom on external strings, tokens, redirects, raw SQL, deserialization, outbound HTTP or security headers, or when asked to review code for security — injection, XSS, atom exhaustion, SSRF, open redirects,
name: security-essentials description: "Use when Elixir/Phoenix code handles params or other user input, String.to_atom on external strings, tokens, redirects, raw SQL, deserialization, outbound HTTP or security headers, or when asked to review code for security — injection, XSS, atom exhaustion, SSRF, open redirects, CSP, timing attacks, Sobelow. Not for permission rules (use phoenix-authorization-patterns)."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2, Ecto 3.14 — October 2026
1. **Never use `String.to_atom/1` on user input** — atoms are never garbage collected; `String.to_existing_atom/1` raises on unknown input (a 500). Map known strings with a `case` 2. **Never build SQL or identifiers from user input** — `fragment` already rejects interpolated strings at compile time; the real holes are `Repo.query`/`SQL.query` with string interpolation and user-chosen columns (`field(p, ^name)`, `identifier(^name)`, `order_by: ^...`). Use `$1` parameters and allowlist columns with a `case` 3. **Never pass user input to `redirect(external: ...)`** — check the host against an allowlist first. `redirect(to: ...)` already raises on anything but a local path, so keep return paths server-side (session) like phx.gen.auth 4. **Avoid `raw/1`** — HEEx escapes `{@x}`; if HTML is required, sanitize with HtmlSanitizeEx first 5. **Keep secrets out of logs** — `redact: true` on sensitive schema fields, extend `:filter_parameters`, and never pass tokens or credentials as Logger metadata or message text 6. **Compare secrets with `Plug.Crypto.secure_compare/2`** — never `==` or a pattern match 7. **Never `:erlang.binary_to_term/1` untrusted bytes** — it can build functions and atoms; use JSON, or `Plug.Crypto.non_executable_binary_to_term(bin, [:safe])` 8. **Give every signed token a purpose and an explicit `max_age`** — `Phoenix.Token.verify(endpoint, "purpose", token, max_age: ...)`; `Phoenix.Token` can't be revoked, so use database tokens (phx.gen.auth `UserToken`) for login sessions and API keys 9. **Validate outbound URLs (SSRF)** — allowlist scheme and host before `Req.get(url)`, and disable redirects 10. **Production transport and headers** — keep the generated `force_ssl` (HSTS), and replace the default CSP, which only sets `base-uri` and `frame-ancestors` 11. **Run audits after dependency changes** — `mix hex.audit`, `mix deps.audit`, `mix sobelow`
---
The atom table has a fixed limit (default 1,048,576) and is never garbage collected. Filling it crashes the whole VM.
<!-- epg:bad -->
role = String.to_atom(params["role"]) # unbounded atom creation status = String.to_existing_atom(params["status"]) # ArgumentError (500) on unknown input
def parse_role("admin"), do: {:ok, :admin}
def parse_role("member"), do: {:ok, :member}
def parse_role(_), do: {:error, :invalid_role}For schema fields, `Ecto.Enum` does this mapping for you when you `cast`.
---
The query DSL parameterizes `^values`, and `fragment("lower(#{x})")` is a compile error ("to prevent SQL injection attacks, fragment(...) does not allow strings to be interpolated"). The risks that remain:
<!-- epg:bad -->
# Raw SQL built with interpolation — injection
Repo.query("SELECT * FROM users WHERE email = '#{email}'")
# User picks the column: ?sort=hashed_password orders (and leaks) by the password hash
order_by(query, [u], asc: field(u, ^String.to_existing_atom(params["sort"])))
# identifier/1 is quoted, but the user still chooses which column or table is read
fragment("SELECT ? FROM users", identifier(^params["column"]))# Parameters for raw SQL
Repo.query("SELECT * FROM users WHERE email = $1", [email])
# Allowlist user-chosen columns
defp sort_field("name"), do: :name
defp sort_field("inserted_at"), do: :inserted_at
defp sort_field(_), do: :inserted_at
def list_users(params) do
from(u in User, order_by: [asc: field(u, ^sort_field(params["sort"])), asc: u.id])
|> Repo.all()
end`splice(^list)` sends each element as a parameter and is safe. `constant(^value)` inlines the value into the SQL (escaped), so each distinct value creates a new prepared query: don't feed it user input.
---
`redirect(conn, to: path)` raises `ArgumentError` unless `path` starts with a single `/` and contains no `\`, tab or newline. So `to: params["return_to"]` is a crash, not an open redirect. The risk is `external:`, in controllers and in LiveView (`redirect(socket, external: url)`).
<!-- epg:bad -->
def callback(conn, %{"next" => next}) do
redirect(conn, external: next) # https://evil.example/login looks like it came from you
end@allowed_hosts ["docs.example.com", "billing.example.com"]
def callback(conn, %{"next" => next}) do
case URI.new(next) do
{:ok, %URI{scheme: "https", host: host}} when host in @allowed_hosts ->
redirect(conn, external: next)
_ ->
redirect(conn, to: ~p"/")
end
endFor "return to where you were", copy phx.gen.auth: `maybe_store_return_to/1` saves `current_path(conn)` in the session on GET requests and `log_in_user/3` redirects to it with `to:`. The path never comes from a query param.
---
HEEx escapes every `{@value}` in text and attributes. `raw/1` turns that off.
<!-- epg:bad -->
<div>{raw(@user_bio)}</div>
<a href={@user_url}>website</a><div>{@user_bio}</div>
<div>{raw(HtmlSanitizeEx.basic_html(@user_bio))}</div>
<p class="whitespace-pre-line">{@comment_body}</p>· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…