Adze

Hex.pm Docs License: MIT

AI coding agents waste context window reading 2000-line files, then fumble multi-edit refactors across a codebase. Adze fixes both problems.

Adze parses Elixir source as AST, returns a structural skeleton with exact line ranges, and lets the agent read only the lines it needs. When it's time to refactor, the same AST machinery performs safe mechanical transforms — extract, rename, reorder — that would be tedious and error-prone as multi-edits.

Built on Sourceror and Igniter.

Requirements

Getting Started

Pick one of two install paths. Both end up in the same place.

Igniter adds the dependency and sets up agent integration in one step. If your project doesn't already use Igniter, install its archive first:

mix archive.install hex igniter_new

Then:

mix igniter.install adze

This adds the dependency and sets up skill files so your agent knows to reach for mix adze instead of hand-editing:

Platform Skill path
Claude Code .claude/skills/adze/SKILL.md
Codex / Pi / OpenCode .agents/skills/adze/SKILL.md

Both skills are always installed, since Claude Code only loads skills from .claude/skills/ and the other agents from .agents/skills/.

It also adds an adze section to AGENTS.md (creating it if needed). Codex, Pi and OpenCode read AGENTS.md. Claude Code (v2.1.277+) reads it too, but only when the project has no CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md. So if your project has a CLAUDE.md, the installer appends an @AGENTS.md import to it (and removes the adze section older versions wrote there). It never creates a CLAUDE.md and never edits CLAUDE.local.md; if that's your only one, add the @AGENTS.md line yourself.

To upgrade, use mix igniter.upgrade adze instead of mix deps.update adze. It updates the dependency and refreshes the skills and the AGENTS.md section. When a release changes how adze is used, it also lists anything in your own files (scripts, CI, your own agent notes) that needs updating. If you upgraded with mix deps.update, or use adze as a path dependency, run mix igniter.install adze afterwards to refresh the files.

Option 2: Manual

Add adze as a dev dependency:

def deps do
  [
    {:adze, "~> 0.2.0", only: [:dev]}
  ]
end

Then mix deps.get.

Agent integration

After fetching deps, copy the skill file and add a reference to your agent config:

Claude Code:

mkdir -p .claude/skills/adze
cp deps/adze/skill.md .claude/skills/adze/SKILL.md

Codex / Pi / OpenCode:

mkdir -p .agents/skills/adze
cp deps/adze/skill.md .agents/skills/adze/SKILL.md

Then append the contents of agents-snippet.md (also at deps/adze/agents-snippet.md) to your AGENTS.md, creating it if it doesn't exist:

cat deps/adze/agents-snippet.md >> AGENTS.md

After upgrading adze, copy skill.md again and replace the adze section in AGENTS.md. Or switch to Option 1, which does both for you.

When to Reach for Adze

You're about to… Use this instead
Read an Elixir file > 200 lines mix adze ls --file PATH first, then read only the line range you need
Multi-edit a defmodule rename across the project mix adze rename --from Old --to New
Copy a function + its private helpers into a new module by hand mix adze extract --file ... --definition fun/N --module New.Module
Reorder defs inside a module with multi-edit mix adze mv --file ... --definition fun/N --before other/M
Grep for callers of Mod.fun across the project mix adze find-callers --target Mod.fun/N
Convert a def to defp after eyeballing call sites mix adze extract-private --file ... --definition fun/N
Eyeball a module's alias/import/use clauses mix adze aliases --file PATH

If none of these match, fall back to standard read/edit/grep.

Quick Start

# Map a file's structure without reading all of it
mix adze ls --file lib/my_app/accounts.ex

# Find all callers of a function across the project
mix adze find-callers --target MyApp.Accounts.register/1

# Several arities at once (e.g. a function with default arguments)
mix adze find-callers --target MyApp.Accounts.register/1,2

Write, then compile

Write commands write to disk and print a summary: the files they touched with +/- line counts, and anything you need to act on. Every safety check runs before anything is written, so a refused command leaves your files alone. Use git diff for the full changes, or add --dry-run to see them without writing:

$ mix adze extract --file lib/sample.ex --definition greet/1 --module Sample.Greeter --dry-run
# new file: ./lib/sample/greeter.ex (Sample.Greeter)
defmodule Sample.Greeter do
  @spec greet(String.t()) :: String.t()
  def greet(name) when is_binary(name) do
    "hello, " <> name
  end

  def greet(_), do: "hello, stranger"
end

# source diff
--- a
+++ b
@@ -13,13 +13,6 @@

   defstruct [:id, :name, count: 0]

-  @spec greet(String.t()) :: String.t()
-  def greet(name) when is_binary(name) do
-    "hello, " <> name
-  end
-
-  def greet(_), do: "hello, stranger"
-
   defp internal(x), do: x * 2
 ...

# dropped directives (use / import / require)
# add back to the new target module if the build needs them.
  source:6  use GenServer
  source:8  import Enum, only: [map: 2]
  source:9  require Logger

Drop --dry-run to apply it. You get the summary:

$ mix adze extract --file lib/sample.ex --definition greet/1 --module Sample.Greeter
# extract → Sample.Greeter
  created  ./lib/sample/greeter.ex  (8 lines)
  updated  lib/sample.ex  (+0 −7)

# dropped directives (use / import / require)
# add back to the new target module if the build needs them.
  source:6  use GenServer
  source:8  import Enum, only: [map: 2]
  source:9  require Logger

Then compile:

mix adze extract --file lib/sample.ex --definition greet/1 --module Sample.Greeter
mix compile --warnings-as-errors

Command Reference

Read-only (safe, never writes)

Command Description
mix adze ls --file PATH Structural outline with line ranges
mix adze deps --file PATH Intra-module call graph (caller → callees)
mix adze ls-deps --file PATH --definition fun/N Transitive dependency tree from one function
mix adze ls-extract --file PATH --definition fun/N Private closure that would move with a function
mix adze aliases --file PATH All alias/import/require/use per module
mix adze find-callers --target Mod.fun/N Project-wide callers (resolves aliases, pipes, captures)

Write (refactoring)

Each write op writes to disk and prints a summary. Add --diff to print the full diffs too, or --dry-run to print them without writing.

Command Description
mix adze mv --file PATH --definition fun/N --before other/M Reorder a definition within a module
mix adze extract --file PATH --definition fun/N --module New.Mod Extract function + closure into a new module
mix adze rename --from Old.Mod --to New.Mod Rename a module across the entire project
mix adze extract-private --file PATH --definition fun/N Flip def → defp when no external callers exist
Example output: mix adze ls
$ mix adze ls --file lib/sample.ex
# lib/sample.ex
defmodule Sample   L1–40
  @moduledoc   L2–4
  use GenServer   L6
  alias Sample.Inner   L7
  import Enum   L8
  require Logger   L9
  @behaviour GenServer   L11
  @some_attr   L12
  defstruct [:id, :name, :count]   L14
  @spec greet   L16
    greet/1 when …   L17–19
    greet/1   L21
  p internal/1   L23
  m debug/1   L25–29
  g is_pos/1 when …   L31
  d child_call/1   L33

Each line is a definition with its line range. A one-character marker column precedes the name: p = defp, m = defmacro, g = defguard, d = defdelegate; public defs have a blank marker (hence the deeper indent). @spec greet is the spec for greet. Use --format json for the same data as a structured tree.

Configuration

Custom attachable attributes

Some libraries introduce attributes that semantically belong to the next def (e.g. Oban's @job, Decorator's @decorate, Absinthe's @desc). Tell adze to treat them as part of the definition group:

# config/config.exs
config :adze, include_attrs: [:job, :decorate, :desc]

Output format

Every command accepts --format json for programmatic consumption. Text (default) is optimized for humans and LLMs reading inline.

How It Works

Elixir is homoiconic — source code is data. Sourceror gives us a zipper over the AST with formatting and comments preserved. Every structural operation is a tree walk, not string manipulation.

Single-file analysis (outline, deps, aliases) uses raw Sourceror. Project-wide operations (rename, extract caller rewriting, find-callers) use Igniter for safe multi-file rewrites with formatting preservation.

Limitations

find-callers (and by extension extract-private) resolves qualified calls, pipe variants, and &Mod.fun/n captures — including alias resolution. It won't find:

Because callbacks and dynamically dispatched functions have no visible callers, extract-private always refuses @impl definitions and callbacks of a loadable @behaviour / used behaviour, and refuses anything in a module whose use / @behaviour target it can't load as a behaviour (e.g. use MyAppWeb, :controller) unless you pass --force. Functions reached via mod.fun() / apply/3 from elsewhere are still invisible to it.

When in doubt, run mix compile --warnings-as-errors after any write op. The compiler catches what adze can't.

Documentation

Full API documentation is available on HexDocs.

Prior Art & Acknowledgments

Adze exists because of the tools it builds on and the project that showed the way:

The guiding principle, borrowed directly from clj-surgeon:

The tool does the mechanical work. The AI decides what to do. The compiler catches mistakes. The AI fixes what the compiler reports.

License

MIT