Claude Code Elixir Setup (and Codex): Connect Tidewave to a Phoenix App, Write LiveView Rules, and Run a mix format, compile and test Loop

Claude Code Elixir setup for Phoenix: connect Tidewave over HTTP, write a LiveView CLAUDE.md, and wire a mix format, compile and test loop into a hook.

Tidewave's Claude Code documentation page on HexDocs, showing the instruction to add Tidewave with claude mcp add --transport http tidewave http://localhost:$PORT/tidewave/mcp.
Tidewave's Claude Code page on HexDocs (Tidewave v0.9.2), which documents adding the Tidewave MCP server with claude mcp add over the HTTP transport and checking the connection with /mcp. Source: Tidewave documentation on HexDocs. · Original source

Ask a coding agent to add a renew-loan button to a Phoenix app and you can get code written for a Phoenix that no longer exists: a helper renamed releases ago, a LiveView callback shaped by an old tutorial, a test that quietly reads your development database. For Claude Code Elixir work, the fix is not a longer prompt. It is three pieces of setup: a connection into the running application, written house rules, and a verification loop that runs after every edit.

This tutorial builds all three on an existing Phoenix 1.7+ app with LiveView and Ecto. You will add the Tidewave MCP server as a development-only dependency, connect Claude Code to it over HTTP, write one instruction file that Claude Code and Codex both read, and wire mix format, mix compile --warnings-as-errors and mix test into a loop backed by a PostToolUse hook. Every step ends with what you should observe when it works and what a failure looks like.

Why now: on October 30, 2026, José Valim posted on X that Tidewave can now run on CI, taking screenshots, recording captioned videos and producing framework diagnostics for Phoenix and Rails. CI gets a short section near the end. The local loop is what you will use every day, and the CI mode is built from the same pieces.

A note on evidence. Commands and configuration follow the documentation linked in each section. The example app, Shelf, a small library-lending app with Catalog, Accounts and Loans contexts, is illustrative. No command output in this guide is presented as observed, except the sample that the Mix documentation itself prints. Expected observations are what the documentation and the scripts’ own logic predict.

What you need before you start

  • [ ] Elixir and Erlang/OTP at the versions your project expects, and the same versions in CI. The formatter’s output can differ between Elixir versions, so a mismatch turns a formatting check into noise.
  • [ ] An existing Phoenix 1.7+ project under git, with a passing mix test and a dev database that mix phx.server can reach.
  • [ ] A current Claude Code install, signed in. Codex CLI is optional; the instruction file and MCP notes below cover it too.
  • [ ] jq on your PATH. The hook script parses Claude Code’s JSON input with it, as the official hook examples do.
  • [ ] Permission to edit and commit mix.exs, lib/shelf_web/endpoint.ex, config/dev.exs, AGENTS.md, CLAUDE.md and .claude/settings.json.
  • [ ] A clean working tree, so each step lands as its own reviewable, revertible commit. Note which of AGENTS.md, CLAUDE.md and .claude/settings.json already exist; rollback treats new and existing files differently.

The overall shape will be familiar if you followed our Vue and Nuxt setup or the Expo guide. The Elixir difference is where the MCP server lives. Tidewave runs inside your Phoenix app, so the agent is not reading a documentation mirror. It is querying the application you are developing, at the exact dependency versions in your mix.lock.

Step 1: Add Tidewave to the Phoenix app, for development only

The Tidewave Phoenix README describes it as an MCP server that gives coding agents runtime-level tools for Phoenix development. Those tools can execute code inside your app, so the whole setup is scoped to the :dev environment and to localhost.

  1. Add the dependency to mix.exs with only: :dev.
  2. In lib/shelf_web/endpoint.ex, add a guarded plug Tidewave directly above the existing if code_reloading? do block.
  3. If you run Phoenix LiveView 1.1 or later, enable the two debug options in config/dev.exs. Phoenix 1.8+ apps have them on by default.
  4. Run mix deps.get, start mix phx.server, and note the port your endpoint reports. Shelf uses 4000.
# mix.exs
defp deps do
  [
    {:tidewave, "~> 0.9", only: :dev},
    # ...existing deps: phoenix, phoenix_live_view, ecto_sql and the rest
  ]
end
# lib/shelf_web/endpoint.ex (excerpt)
  if Mix.env() == :dev do
    plug Tidewave
  end

  if code_reloading? do
    # existing code-reloading plugs stay exactly as they were
  end
# config/dev.exs
config :phoenix_live_view,
  debug_heex_annotations: true,
  debug_attributes: true

Why each choice is there

only: :dev keeps the package out of test and production builds. The Mix.env() == :dev guard is the second fence: the endpoint only references Tidewave where the dependency exists. The README notes that Tidewave works best with LiveView 1.1 or later, and the two debug options give it HEEx annotations to work with.

The security defaults matter more than the install. According to the README, Tidewave only allows requests from localhost by default, even when your server listens on other interfaces. The :allow_remote_access option removes that restriction; the README says to use it with extreme care and only with an authentication proxy in front of the app. Leave it unset. The :allowed_origins option checks Origin and Host headers against cross-origin and DNS rebinding attacks and defaults to your endpoint’s host. If you use a Content-Security-Policy, Tidewave adds unsafe-eval to script-src and disables frame-ancestors wherever it is loaded, which is one more reason that place must be development only.

If you prefer a generator, the README documents an Igniter route: mix archive.install hex igniter_new, then mix igniter.install tidewave. Review its diff against the three blocks above before committing. In an umbrella project, make these edits in the app that defines the Phoenix endpoint, typically apps/your_app_web.

What you should observe

The dev server starts as before. The plug’s :toolbar option defaults to true, so expect a Tidewave toolbar injected into HTML pages in development; plug Tidewave, toolbar: false turns it off. Then prove the guard with MIX_ENV=test mix compile. It should succeed, because the guarded plug is skipped where the dependency is absent. If the test-environment compile fails with an error naming Tidewave, the plug sits outside the Mix.env() == :dev block. Move it back inside before going further.

Step 2: Connect Claude Code and confirm the runtime tools

Tidewave’s Claude Code page on HexDocs documents a one-line connection over the HTTP transport.

  1. Keep mix phx.server running in its own terminal. The MCP endpoint exists only while the app is up.
  2. From the project root, add the server, replacing 4000 with your endpoint’s port.
  3. Start claude and run /mcp. Look for the status ✔ connected next to tidewave.
  4. If the status is anything else, check the transport first. The page says a 405 error most likely means an incorrect transport, and Phoenix uses HTTP.
  5. Do not choose the Authenticate option. Tidewave runs on your machine, accepts only local connections by default and implements none of the protocol’s authentication endpoints, so selecting it leads to errors.
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp

Tidewave's Claude Code documentation page on HexDocs, showing the instruction to add Tidewave with claude mcp add --transport http tidewave http://localhost:$PORT/tidewave/mcp.
Tidewave's Claude Code page on HexDocs (Tidewave v0.9.2), which documents adding the Tidewave MCP server with claude mcp add over the HTTP transport and checking the connection with /mcp. Source: Tidewave documentation on HexDocs. · Original source

claude mcp add also accepts a scope that decides whether the entry is private to you or shared with the team. The URL embeds a port that can differ between developers, so a personal scope is usually the safer default. Our MCP scopes guide covers that choice and the generic connection failures in more depth.

What the agent can now do

The README lists five tools. The right column shows how each one earns its place in the Shelf example.

Tool What the README says it does Use in the Shelf app
project_eval Evaluates Elixir code inside the running app, like an IEx session for the agent Call Shelf.Loans.renew/1 on a seeded loan and inspect the return value before writing the LiveView that uses it
execute_sql_query Runs SQL against the development database Confirm that a renewal changed the due date in dev data
get_docs Returns documentation for a module or function at the versions locked in mix.lock Read Phoenix.LiveView and Ecto.Changeset docs for the versions Shelf actually uses
get_logs Reads logs written by the server Read the request log and backtrace after a button click misbehaves
get_source_location Finds where a module or function is defined, in the app or a hex dependency Jump to a component definition instead of grepping deps/

Test the connection with one request. Ask Claude to use the Tidewave get_docs tool to show the documentation for Ecto.Changeset.validate_required/3 and to say where the text came from. Success is a visible tidewave tool call in the transcript before the answer. If Claude answers from memory with no tool call, either the server is not connected or nothing told Claude when to reach for it. Recheck /mcp, then make Step 3’s instruction file explicit about it.

Codex

The README lists Codex among clients with dedicated setup instructions, so follow that page first. For reference, Tidewave’s CI guide configures Codex with a [mcp_servers.tidewave] table and a url key. The same table pointed at your local endpoint is the minimal form:

# ~/.codex/config.toml (adapted from the Codex block in Tidewave's CI guide)
[mcp_servers.tidewave]
url = "http://localhost:4000/tidewave/mcp"

The CI example adds required = true and pre-approves Tidewave’s tool calls because a CI runner cannot answer approval prompts. On your own machine, leave approvals interactive.

Worktrees

If you run several git worktrees, each dev server probably listens on its own port, so no single URL serves them all. The README suggests registering mix tidewave.proxy as a STDIO MCP server instead. It adds a port parameter to every tool and dispatches each call to the right application.

Step 3: Write one instruction file for Claude Code and Codex

An instruction file turns your team’s Phoenix habits into context the agent loads every session. Claude Code’s memory documentation says it reads a repository’s AGENTS.md when no CLAUDE.md exists, on v2.1.277 or later, and that a CLAUDE.md containing an @AGENTS.md import works in sessions where direct reading is unavailable. OpenAI’s AGENTS.md guide for Codex says Codex reads AGENTS.md files from the project root down to the working directory. So keep the shared rules in AGENTS.md, and put Claude-only notes in a short CLAUDE.md that imports it.

The file below is a complete, copyable example for the illustrative Shelf app. Replace the module names and paths with yours, but keep the structure: project map, look-up rules, hard rules, conventions, verification.

# AGENTS.md: Shelf (Phoenix 1.7+, LiveView, Ecto)

## Project map
- Contexts live in lib/shelf/: Shelf.Catalog (books, copies), Shelf.Accounts (members), Shelf.Loans (checkouts, renewals, due dates).
- The web layer lives in lib/shelf_web/. LiveViews and controllers call context functions; they never call Shelf.Repo directly.
- Tests mirror lib/ under test/. Shared fixtures are in test/support/fixtures/.

## Look things up before writing code
- Before using any Phoenix, Phoenix.LiveView, Ecto or other hex dependency function, read its docs with the Tidewave get_docs tool. Our versions are pinned in mix.lock; do not rely on memory of older releases.
- Use get_source_location to find a module definition instead of searching deps/.
- Use project_eval to check what a context function returns in the running dev app before relying on its shape.
- After reproducing a bug in the browser, read get_logs before changing code.

## Hard rules
- Do not add, remove or upgrade a dependency in mix.exs. Propose the change and wait for approval.
- Never edit a migration in priv/repo/migrations/ that already exists on main. Write a new migration.
- execute_sql_query is for reading the dev database only. No INSERT, UPDATE, DELETE or DDL through it.
- Do not silence warnings: no @compile directives and no underscore-renaming of variables you do not understand. Fix the cause or ask.
- Never set :allow_remote_access on the Tidewave plug and never move the plug outside the Mix.env() == :dev block.
- The dev server already runs on port 4000. Do not start another one.

## LiveView conventions in this app
- Socket assigns hold what the template renders. Load lists through a context function with a limit; never assign a whole table.
- handle_event callbacks call one context function and assign the result. Business rules live in contexts, where they are tested.
- Every new LiveView gets a test in test/shelf_web/live/ that mounts it and asserts on the rendered HTML.
- Shared function components live in lib/shelf_web/components/. Check for an existing component before adding one.

## Verify after every change, in this order
1. mix format --check-formatted
2. mix compile --warnings-as-errors   (fast dev check)
3. MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors <the test file that covers your change>
4. After a fix: MIX_ENV=test mix do compile --warnings-as-errors + test --failed
5. Before reporting done: MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors
Never use plain mix test as the check: it does not fail on compile warnings.
Paste the final lines of each command's output in your last message.
@AGENTS.md

## Claude Code
- Tidewave tools appear as mcp__tidewave__<tool>. If /mcp does not show tidewave as connected, stop and say so instead of guessing an API.
- A PostToolUse hook formats each edited Elixir file and returns format errors and compile warnings to you. Treat that feedback as a failing check.

Why each rule earns its line

Rule Why it is there Sign it was ignored
Read docs with get_docs first It reads the versions locked in mix.lock; memory of older releases is how outdated calls appear A compiler warning about an undefined or deprecated function
No new dependencies without approval A new hex package changes mix.lock and your supply chain git diff main -- mix.exs mix.lock is not empty
Never edit a merged migration Databases that already ran it never see the edit git diff --name-status main -- priv/repo/migrations shows M instead of A
execute_sql_query reads only The README says it can run any SQL against dev; writes bypass contexts and migrations Dev data changes that no migration or context call explains
Do not silence warnings --warnings-as-errors only helps when warnings are fixed, not hidden New underscore variables or @compile attributes in the diff
Verify in a fixed order, tests behind a test-env compile Cheap checks first; tests run only after a MIX_ENV=test compile that fails on warnings A done message with no command output, or plain mix test as the last check

The SQL rule is a request, not a control. Claude Code’s memory page is explicit that CLAUDE.md content is context rather than enforced configuration. For a database an agent must never write to, a database role is the stronger control, as our Postgres read-only guide shows. For a disposable dev database you can reseed, the written rule is usually enough.

Confirm both agents loaded it

  1. In Claude Code, run /context all and check that CLAUDE.md appears under Memory files. Imported files load with the file that imports them.
  2. Ask Claude what it must do before using a LiveView function. The answer should cite the get_docs rule.
  3. In Codex, from the project root, run the check from OpenAI’s guide shown below. The documented expectation is that Codex quotes items from the loaded instructions.
  4. Keep the files short. The memory page targets under 200 lines per file for adherence, and Codex stops adding instruction files once their combined size reaches 32 KiB by default.
codex --ask-for-approval never "Summarize the current instructions."

Step 4: Run format, compile and test after every change

Instructions describe what good looks like. The loop proves it. Run these commands from the project root in this order. Their behaviour comes from the Mix v1.20.5 documentation for mix format, mix compile and mix test.

# 1. Formatting check: exits non-zero and writes nothing if a file is unformatted
mix format --check-formatted

# 2. Fast dev check while you iterate (the Step 5 hook runs it after lib edits)
mix compile --warnings-as-errors

# 3. The gate: compile in the test environment, then run the touched test file
MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors test/shelf/loans_test.exs

# 4. After a fix: rerun only what failed last time, behind the same compile gate
MIX_ENV=test mix do compile --warnings-as-errors + test --failed

# 5. Before reporting done: the full documented form
MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors

The order is deliberate. A formatting check is the cheapest and, in check mode, writes nothing. The dev compile in line 2 is a fast signal, not the gate. Plain mix test compiles the project again in the test environment, and that compile does not fail on warnings: the mix test documentation says its own --warnings-as-errors option only applies to test files and gives the combined MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors form for compilation and tests together. Code that only compiles under test, such as shared modules under test/support/ in a typical generated Phoenix project or a branch guarded by Mix.env() == :test, never passes through the dev compile at all.

So every test run in this loop, including the scoped iterations, sits behind a test-environment compile with --warnings-as-errors. A warning anywhere in that compile stops the command before any test runs. Scoping to one file keeps each round fast, and --failed reruns only what broke.

The two halves of the gate catch different warnings. The compile --warnings-as-errors half covers modules the test environment compiles, such as test/support/ in a typical generated project. The *_test.exs files are not compiled by that step; mix test loads them, so a warning inside a test file is caught by the test --warnings-as-errors half, which exits 1 when the tests otherwise pass.

Command Passing run Failing run Agent’s next move
mix format --check-formatted Exits 0 Exits non-zero; formatted contents are not written to disk Run mix format on the file, then check again
mix compile --warnings-as-errors Exits 0 Exits non-zero when the dev compile has one or more project warnings Fix the warning at the reported file and line
MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors test/shelf/loans_test.exs Compile exits cleanly and the summary line reports no failures The compile step exits non-zero on a test-env warning before any test runs; otherwise test exits 1 for test-file warnings only, 2 for failures, 3 for both Fix the warning; for a failure rerun the reported FILE:LINE behind the same compile gate
MIX_ENV=test mix do compile --warnings-as-errors + test --failed Previously failing tests now pass The same failure repeats Read the assertion’s left and right values before editing again

Reading a failure

The mix test documentation prints this sample of a failing run. It is the documentation’s output, not a Shelf run:

$ mix test
Running ExUnit with seed: 646219, max_cases: 16
...

  1) test greets the world (FooTest)
     test/foo_test.exs:5
     Assertion with == failed
     code:  assert Foo.hello() == :world!
     left:  :world
     right: :world!
     stacktrace:
       test/foo_test.exs:6: (test)

........

Finished in 0.05 seconds (0.00s async, 0.05s sync)
1 doctest, 11 tests, 1 failure

Each part tells the agent something. The counter and name say which test failed. The test/foo_test.exs:5 line is a ready-made rerun target: pasting it after mix test runs only that test, so in this loop the agent runs MIX_ENV=test mix do compile --warnings-as-errors + test test/foo_test.exs:5. Left and right show the actual and expected values. Tell the agent to paste this block back verbatim instead of summarising it; the location line is the cheapest rerun you can get.

Matrix chart with four mix commands as rows and five problem types as columns. Indigo pills mark failing exits: mix format --check-formatted fails on unformatted code, mix compile --warnings-as-errors fails on a lib warning, mix test exits 2 on a failing assertion, and mix test --warnings-as-errors exits 1 on a test-file warning and 2 on a failing assertion. The dependency-warning column reads no in every row.Matrix chart with four mix commands as rows and five problem types as columns. Indigo pills mark failing exits: mix format --check-formatted fails on unformatted code, mix compile --warnings-as-errors fails on a lib warning, mix test exits 2 on a failing assertion, and mix test --warnings-as-errors exits 1 on a test-file warning and 2 on a failing assertion. The dependency-warning column reads no in every row.
No single command catches every problem: format catches style, compile catches project warnings, and mix test with --warnings-as-errors adds test-file warnings; dependency warnings fail none of them. Source: Mix v1.20.5 task docs for mix format, compile and test on hexdocs.pm, read Nov 5, 2026.

Three documented details save wasted rounds:

  • mix compile --warnings-as-errors only concerns compilation warnings from your project, not its dependencies. A noisy dependency will not fail the build, so the agent has no reason to touch deps/.
  • --all-warnings is on by default, so compile prints warnings from previous compilations too. A warning in a file nobody touched this session still fails the check.
  • Failures reported by mix test --warnings-as-errors cannot be retried with --failed. After fixing a test-file warning, rerun the file itself.

mix test --stale is an alternative to naming a file. It runs only test files that reference modules changed since the last --stale run; the first run executes everything to build its manifest. Keep it behind the same gate: MIX_ENV=test mix do compile --warnings-as-errors + test --stale. Giving the agent a check it can run is the core habit in our Claude Code prompting guide. Elixir makes it unusually crisp: a handful of commands, each with a documented exit status.

Step 5: Enforce the loop with a PostToolUse hook

The memory documentation is direct about the limit of instruction files: anything that must happen at a fixed point, such as after each file edit, belongs in a hook, because hooks run regardless of what Claude decides. Claude Code’s hooks guide documents a PostToolUse hook with an Edit|Write matcher for exactly this case, formatting a file after Claude edits it. The version below checks the formatter’s result, adds a dev compile for application files and hands any failure back to Claude.

  1. Save the script below as .claude/hooks/mix-check.sh.
  2. Make it executable with chmod +x .claude/hooks/mix-check.sh.
  3. Register it in the project’s .claude/settings.json, adding to any PostToolUse entries that already exist rather than replacing them.
  4. Test it outside Claude Code with the five inputs in the table below.
  5. Run /hooks in Claude Code and confirm it appears under PostToolUse.
  6. Ask Claude for a small edit to a context module and confirm the file comes back formatted.
#!/bin/bash
# .claude/hooks/mix-check.sh
# After Claude edits an Elixir file: format that file; for application files,
# compile the project in dev. Any failure goes back to Claude as one JSON
# object. Stdout carries JSON only.

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

case "$FILE_PATH" in
  *.ex|*.exs|*.heex) ;;
  *) exit 0 ;;   # not an Elixir file: no decision
esac

cd "$CLAUDE_PROJECT_DIR" || exit 0

block() {
  jq -n --arg reason "$1" '{decision: "block", reason: $reason}'
  exit 0
}

# 1. Format only the edited file. Capture its output and keep its exit status.
if ! FMT=$(mix format "$FILE_PATH" 2>&1); then
  block "mix format failed on $FILE_PATH; fix the file before anything else:
$(echo "$FMT" | tail -n 40)"
fi

# 2. Test files are loaded by mix test, not built by mix compile.
#    The MIX_ENV=test gate in Step 4 checks them.
case "$FILE_PATH" in
  *_test.exs|test/*|*/test/*) exit 0 ;;
esac

# 3. Application files: any project warning makes this exit non-zero.
if ! OUT=$(mix compile --warnings-as-errors 2>&1); then
  block "mix compile --warnings-as-errors failed after editing $FILE_PATH:
$(echo "$OUT" | tail -n 40)"
fi

exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/mix-check.sh",
            "timeout": 180
          }
        ]
      }
    ]
  }
}

Test the script by piping sample hook input into it, as the hooks guide does. Run this from the project root, once per row of the table, setting FILE to that row’s input:

FILE=lib/shelf/loans.ex
jq -n --arg f "$FILE" '{tool_name: "Edit", tool_input: {file_path: $f}}' \
  | CLAUDE_PROJECT_DIR="$PWD" .claude/hooks/mix-check.sh
echo $?

The expected observations follow from the script and the Mix docs; they are not recorded output. Make the prepared change first, and undo it with git restore on that file before the next row.

FILE input Change you make first Expected stdout Expected echo $?
lib/shelf/loans.ex None; the project compiles cleanly Nothing 0
lib/shelf/loans.ex Add an unused variable inside a function One JSON object with "decision": "block"; its reason starts mix compile --warnings-as-errors failed after editing lib/shelf/loans.ex and quotes the warning 0
lib/shelf/loans.ex Delete the module’s final end, so the file no longer parses One JSON object with "decision": "block"; its reason starts mix format failed on lib/shelf/loans.ex; the compile step never runs 0
test/shelf/loans_test.exs Add an unused variable inside a test Nothing; the file is formatted and the hook stops before compiling. The warning appears at the Step 4 gate, where test --warnings-as-errors exits 1 0
README.md Anything Nothing; not an Elixir file 0

The exit status stays 0 in every row because the JSON carries the decision, not the exit code.

Why the script is shaped this way

  • The matcher is case-sensitive and fires only for the Edit and Write tools. The hooks guide warns that Claude can also change files through shell commands; if you need every change covered, add a Stop hook that checks the working tree once per turn.
  • The formatter’s exit status is checked first. If mix format fails, the hook stops there and returns the formatter’s error as a block decision, so a file the formatter rejected, template or not, is never followed by a reassuring clean compile.
  • Test files get formatting only. mix compile does not build them and a dev compile would not see their warnings, so the hook exits silently and leaves them to the MIX_ENV=test gate in Step 4, which the instruction file requires before any done message.
  • Success is silent. The guide notes that a successful hook shows nothing in the conversation, so you confirm it by its effect: a reformatted file.
  • Every failure prints one JSON object built with jq, using the top-level decision: "block" field the guide documents for PostToolUse. A PostToolUse hook cannot undo the edit; the point is to put the error in front of Claude before its next step, so it can fix the cause.
  • Formatter and compiler output are captured into variables rather than printed, because the guide warns that anything on stdout ahead of the JSON makes Claude Code read it as plain text and ignore the decision.
  • timeout is in seconds. It caps how long a cold compile on a large app can hold up the session.

Template files need one more check, and it is about plugins, not inputs. The format docs define :inputs as the default files mix format uses when you pass none, which is what mix format --check-formatted in Step 4 relies on. The hook passes the edited file explicitly, so :inputs does not decide whether it gets formatted. What decides it for .heex is :plugins: the docs describe plugins that declare the extensions they format, and note that loading plugins compiles the project first. So check two separate things in your generated .formatter.exs. A plugin that handles .heex makes the hook able to format templates. A .heex pattern in :inputs makes the whole-project check include them. If no plugin is listed, drop *.heex from the hook’s first case pattern.

Hooks are the deterministic layer beside permission rules. If you also want to limit which commands the agent may run, treat that as written policy rather than a per-session choice, as our permission modes guide argues.

Loop diagram with seven nodes: Read docs, Edit, Hook (format and dev compile for lib files), Test gate (MIX_ENV=test compile --warnings-as-errors for modules such as test/support), Test file (mix test --warnings-as-errors on the *_test.exs file), Done, and an amber Fix the cause node. Dashed amber arrows return to Edit.Loop diagram with seven nodes: Read docs, Edit, Hook (format and dev compile for lib files), Test gate (MIX_ENV=test compile --warnings-as-errors for modules such as test/support), Test file (mix test --warnings-as-errors on the *_test.exs file), Done, and an amber Fix the cause node. Dashed amber arrows return to Edit.
Illustrative order for the Shelf example: the PostToolUse hook formats every edited file and compiles lib edits in dev; the MIX_ENV=test compile gate stops warnings in compiled test-only modules such as test/support, and mix test --warnings-as-errors stops warnings in the loaded *_test.exs files. Original diagram based on the Claude Code hooks guide, the tidewave_phoenix README and the Mix v1.20.5 task docs, read Nov 5, 2026.

Step 6: Map common Phoenix agent mistakes to a rule and a check

Rules without checks drift, and checks without rules confuse the agent. This filled table pairs the mistakes agents commonly make in a Phoenix codebase with the line in AGENTS.md that prevents each one and the command that catches it when prevention fails.

Mistake What it looks like in Shelf Rule that prevents it Check that catches it
Outdated Phoenix or LiveView API A LiveView calls a function shape from an older release Read docs with get_docs first The hook’s dev compile, then the MIX_ENV=test compile gate
Warning left behind An unused alias in Shelf.Loans after a refactor Do not silence warnings mix compile --warnings-as-errors exits non-zero
Warning in test-only code An unused variable in a module under test/support/ Tests run behind the MIX_ENV=test gate The gate’s compile step exits non-zero before any test runs
Test leans on dev data A test asserts on rows the agent inserted while exploring Tests run in MIX_ENV=test; the SQL tool only reads The test fails on CI, where no dev data exists
Business logic in handle_event Renewal rules written inline in the LiveView handle_event calls one context function No new test in test/shelf/loans_test.exs during review
Whole table in assigns assign(socket, :loans, Repo.all(Loan)) in mount Lists load through a context function with a limit git grep "Repo\." lib/shelf_web returns matches
Edited a merged migration A column type changed inside an old file New migration only git diff --name-status main -- priv/repo/migrations shows M
Unapproved dependency A date library added to fix a renewal bug Propose and wait git diff main -- mix.exs mix.lock is not empty

Read the table from the right when something slips through. If a check fires, the rule beside it is the one to tighten. If a mistake reaches review with no check firing, add one: the git commands in the last column are cheap enough to run before every merge.

Troubleshoot Tidewave, the hook and the test loop

Symptom Likely cause Fix Expected after the fix
/mcp shows tidewave as failed or not connected mix phx.server is not running, or the URL has the wrong port Start the server; if the port differs, remove the entry and add it again with the right port ✔ connected
A 405 error Wrong transport Add the server again with --transport http ✔ connected
Errors after choosing Authenticate Tidewave implements no authentication endpoints Do not authenticate; remove the entry and add it again Connected with no auth step
Tidewave tools disappear mid-session The app stopped or is restarting after a config or dependency change; the CI guide notes the tools are unavailable until it is back Restart mix phx.server, wait a few seconds, run /mcp again Tools listed again
Dozens of old warnings fail the compile after you adopt --warnings-as-errors --all-warnings is on by default and reprints warnings from earlier compilations Fix the existing warnings in one separate commit before enabling the hook A clean mix compile --warnings-as-errors
The hook returns mix format failed on ... The edited file no longer parses, or a template has no formatter plugin Fix the syntax error the reason quotes; for templates, add a plugin or drop *.heex from the hook The next edit returns no JSON
Tests pass locally but a warning in test/support/ surfaces later The agent ran plain mix test, which does not fail on compile warnings, and the dev compile never builds test-only code Run tests only through MIX_ENV=test mix do compile --warnings-as-errors + test ... The gate stops at the warning; clean once it is fixed
A test passes locally but fails on CI with missing records The test depends on dev data created through Tidewave Build the data inside the test with fixtures; keep tests in MIX_ENV=test The test passes on an empty database
The hook never fires Matcher typo (matchers are case-sensitive), script not executable, or invalid settings JSON Check /hooks, run chmod +x, remove trailing commas The hook is listed under PostToolUse
The hook’s JSON has no effect A shell profile printed text before the JSON Wrap profile echo lines in an interactive-shell check; inspect with claude --debug The decision reaches Claude
A worktree session talks to the wrong app One fixed port in the MCP URL Register mix tidewave.proxy as a STDIO server Tools accept a port parameter

Work top to bottom. Most connection problems are the first two rows, and the documented /mcp status is the fastest signal you have.

Tidewave on CI: what the new guide documents

The Continuous Integration guide behind the October announcement reuses almost everything above. What it documents, in brief:

  • Tidewave joins a CI run through Tidewave Connect and an existing harness, Anthropic’s claude-code-action or OpenAI’s codex-action. The worked examples target GitHub Actions; other CI systems follow a manual setup.
  • Because Tidewave is a dev-only dependency, the workflow runs mix setup and then the Phoenix server in the dev environment, against a PostgreSQL service, inside a loop that restarts the server whenever it exits.
  • A tidewave-ai/connect-action@v1 step needs a TIDEWAVE_CONNECT_KEY secret and outputs the MCP URL. The Claude step passes it through --mcp-config as an http server and allows mcp__tidewave,Bash(mix:*),Bash(MIX_ENV=test mix:*).
  • GITHUB_TOKEN and GitHub App tokens cannot currently upload media attachments to comments, so posting screenshots or videos needs a personal access token from a bot user. Vision mode itself works without one.
  • On a publicly accessible repository, the guide recommends restricting who can trigger the jobs, for example with a job-level if on github.event.sender.login.
  • The CI database starts empty, so keep seeds current and tell the agent how to sign in.

The top of the Continuous Integration page in Tidewave's HexDocs documentation.
The opening of Tidewave's Continuous Integration guide on HexDocs (v0.9.2), which covers running Tidewave in CI with Tidewave Connect and an agent harness such as claude-code-action or codex-action. Source: Tidewave documentation on HexDocs. · Original source

Notice what carries over: the dev-only plug, the HTTP transport and mix commands in the allowlist, including the MIX_ENV=test mix form the Step 4 gate uses. A local loop that already passes cleanly is the prerequisite for a CI agent worth reading.

Roll back the setup

Rollback depends on whether each file existed before Step 1 and whether you committed the setup. git restore only returns a tracked file to its last committed state: it does not delete files the setup created, and after a setup commit it restores the setup version.

  1. Remove the MCP entry with claude mcp remove tidewave, and delete the [mcp_servers.tidewave] table from ~/.codex/config.toml if you added it. /mcp should no longer list tidewave.
  2. Run git status --short and sort every setup file into one row of the table below.
  3. Apply that row’s command, then run mix deps.get.
  4. Check the hook separately. /hooks should no longer list mix-check.sh, and any hooks you had before the setup should still be listed. To pause every hook temporarily instead, the hooks guide documents "disableAllHooks": true.
  5. Prove the app is back where it started: mix compile --warnings-as-errors and MIX_ENV=test mix do compile --warnings-as-errors + test should pass exactly as they did before Step 1.
Situation Files it usually covers Command Expected result
You committed the setup in its own commit or commits Everything the setup touched git revert <setup-commit> for each setup commit, newest first Files the commits created are deleted and files they modified return to their prior state, in a new commit
Uncommitted change to a file that existed before mix.exs, mix.lock, lib/shelf_web/endpoint.ex, config/dev.exs, plus AGENTS.md, CLAUDE.md or .claude/settings.json if they already existed git restore <those files> git status no longer lists them
Uncommitted file the setup created git status --short shows ??: often AGENTS.md, CLAUDE.md, .claude/hooks/mix-check.sh and possibly .claude/settings.json rm <those files>, after reading each one to confirm it holds only setup content git status no longer lists them
The hook landed in a settings file that also holds unrelated hooks, in a commit you cannot simply revert .claude/settings.json Edit the file: delete only the PostToolUse object whose command ends in mix-check.sh, and the PostToolUse key only if it is then empty; delete the script Your other hooks still load; /hooks no longer shows mix-check.sh

If one agent edit went wrong rather than the setup itself, Claude Code checkpoints are faster than git; our checkpoints and /rewind guide explains what they track and what still needs git.

Success and failure checks

Call the setup done when every box in the first list is ticked:

  • [ ] /mcp shows tidewave as ✔ connected while mix phx.server runs.
  • [ ] Before using an unfamiliar API, the transcript shows a Tidewave get_docs call.
  • [ ] After each change, MIX_ENV=test mix do compile --warnings-as-errors + test --warnings-as-errors passes for the touched test file.
  • [ ] mix format --check-formatted exits 0 and the diff contains no formatting-only churn.
  • [ ] The five manual hook inputs in Step 5 produce the expected stdout.
  • [ ] MIX_ENV=test mix compile succeeds, proving the plug is guarded.

Stop and fix the setup if any of these appear:

  • [ ] Tidewave answers requests from another machine, or :allow_remote_access appears anywhere in the codebase.
  • [ ] mix.exs or mix.lock changed without an approval in the conversation.
  • [ ] A warning disappeared through suppression rather than a code change.
  • [ ] The agent reported done after plain mix test, or without pasting command output.

Sources

YOU'RE THROUGH THIS ONE.

Keep connecting the dots.

Back to the library

Many assistants.
One place to remember.

Bring your AI sessions, context, and usage together with Automater.

Meet Automater