/moai clean
The dead-code identification and safe-removal command. Through static analysis and usage-graph analysis, it finds unused code and removes it safely.
InfoOne-line summary:/moai cleanis a “code diet tool.” It automatically finds and safely deletes unused functions, variables, imports, and files.
InfoSlash command: In Claude Code, type/moai:cleanto run this command directly. Typing just/moaishows the list of all available subcommands.
As a project grows, code that is no longer used piles up. Unused imports, functions that are never called, and unreferenced types make the codebase complicated. /moai clean finds this dead code with static analysis and removes it safely, verified by tests.
From the harness-engineering perspective, this command plays the role of garbage collection. Dead code is a burden not only for humans but also for agents. Every line of code an agent reads is context (tokens), so removing dead code is code hygiene and, at the same time, a way to trim context and save cost.
# Basic usage
> /moai clean
# Preview (check only, no modifications)
> /moai clean --dry
# Remove safe items only
> /moai clean --safe-only
# Analyze specific files/directories only
> /moai clean --file src/auth/
# Analyze specific code types only
> /moai clean --type functions| Flag | Description | Example |
|---|---|---|
--dry (or --dry-run) | Show analysis results only, no removal | /moai clean --dry |
--safe-only | Remove only certain dead code (skip uncertain items) | /moai clean --safe-only |
--file PATH | Analyze only a specific file or directory | /moai clean --file src/utils/ |
--type TYPE | Analyze only a specific code type | /moai clean --type imports |
--aggressive | Include low-usage code (where the single caller is itself dead code) | /moai clean --aggressive |
| Type | Description |
|---|---|
functions | Functions/methods that are never called |
imports | Import statements that are never referenced |
types | Unused type definitions |
variables | Variables declared but never used |
files | Files not imported anywhere |
Previews which items are classified as dead code without modifying any actual code:
> /moai clean --dryThis option is useful when you want to review the analysis results before removal.
/moai clean runs in 7 steps.
flowchart TD
Start["/moai clean run"] --> Phase1["Step 1: static analysis scan"]
Phase1 --> Phase2["Step 2: usage-graph analysis and classification"]
Phase2 --> Classify{"Classification result"}
Classify --> Dead["Certain dead code"]
Classify --> TestOnly["Test-only"]
Classify --> Likely["Likely dead code"]
Classify --> False["False positive (actually in use)"]
Dead --> Phase3{"Step 3: removal-plan approval
(AskUserQuestion / --dry?)"}
Phase3 -->|--dry or reject| Report["Show analysis results and exit"]
Phase3 -->|Approve| Phase4["Step 4: safe removal"]
Phase4 --> Phase5["Step 5: test verification"]
Phase5 --> Pass{"Tests pass?"}
Pass -->|No| Rollback["Roll back and retry"]
Pass -->|Yes| Phase6["Step 6: MX tag cleanup"]
Rollback --> Phase6
Phase6 --> Phase7["Step 7: report"]Step 3 removal-plan approval is a human gate where the orchestrator presents the removal-target list to the user via AskUserQuestion and obtains approval. Step 6 MX tag cleanup also cleans up the @MX comments attached to the removed code, so no dangling comments remain.
Auto-detects the project language by project marker and detects candidates with each language’s standard dead-code analysis tools. It treats the 16 supported languages equally (go, python, typescript, javascript, rust, java, kotlin, csharp, ruby, php, elixir, cpp, scala, r, flutter, swift), gracefully skips tools that are not installed, and silently passes projects with no recognized language marker. The following are representative examples only and do not favor any particular language:
| Language (example) | Analysis tools (example) | Checks |
|---|---|---|
| Go | go vet, staticcheck, deadcode | Unused variables, functions, types |
| Python | vulture, autoflake | Dead code, unused imports |
| TypeScript/JavaScript | ts-prune, ESLint no-unused-vars | Unused exports, variables |
| Rust | cargo clippy, cargo udeps | Dead-code warnings, unused dependencies |
The remaining 12 languages (java, kotlin, csharp, ruby, php, elixir, cpp, scala, r, flutter, swift, etc.) are scanned equally with their own standard toolchains.
Scan categories:
- Unused imports: import statements with no references
- Unused variables: variables declared but never read
- Unused functions: functions defined but never called
- Unused types: type definitions with no usage sites
- Unused files: files not imported anywhere
- Dead dependencies: packages installed but never imported
Builds a usage graph to verify the static analysis results:
- Searches the entire codebase for references to each candidate
- Checks indirect usage (interfaces, reflection, dynamic dispatch)
- Checks test-only usage (used only in tests, unused in production code)
- Checks conditional compilation (build tags, environment-based imports)
| Classification | Description | Removal safety |
|---|---|---|
| Certain dead code | No references anywhere in the codebase | Safe |
| Test-only | Used only in test files | Mostly safe |
| Likely dead code | Low confidence (possible dynamic usage) | Caution needed |
| False positive | Actually in use (reflection, plugins, etc.) | Cannot remove |
Removes in reverse dependency-graph order (leaf nodes first):
- Removes related code as a group (function + private helpers)
- Updates affected imports
- Cleans up files left empty after all exports are removed
- Code with an
@MX:ANCHORtag is never removed without explicit approval
After removal, the full test suite runs to verify against regressions. If tests fail, the removal is rolled back and classified as a “false positive.” Safety is judged by the evidence of passing tests — not by “I deleted it and it seems fine.”
Dead-code removal report
Removed: 15 items (287 lines)
- src/utils/helper.go: UnusedFunction (15 lines)
- src/models/old.go: entire file deleted (120 lines)
Kept (false positives): 2 items
- src/api/handler.go: DynamicHandler (uses reflection)
Test result: PASS (all tests pass)
Codebase reduction:
- Files removed: 3
- Lines removed: 287
- Dependencies removed: 1/moai clean runs by spawning an Agent(general-purpose) refactoring specialist twice (not a dedicated named agent — a general-purpose agent that receives the refactoring whitelist and ANALYZE-PRESERVE-IMPROVE instructions injected at spawn time). Steps 1–2 are one combined spawn, steps 4–5 another, and step 6 the orchestrator handles directly with no spawn.
flowchart TD
User["User request"] --> MoAI["MoAI orchestrator"]
MoAI --> Refactor1["Agent(general-purpose) refactoring specialist
static analysis + usage graph (combined spawn 1)"]
Refactor1 --> MoAI2["MoAI orchestrator
user approval"]
MoAI2 --> Refactor2["Agent(general-purpose) refactoring specialist
safe removal + test verification (combined spawn 2)"]
Refactor2 --> MoAI3["MoAI orchestrator
@MX tag cleanup (direct)"]
MoAI3 --> Complete["Done"]| Agent | Role | Main work |
|---|---|---|
| Agent(general-purpose) refactoring specialist (spawn 1) | Analysis | Static analysis + usage graph (steps 1–2 combined) |
| Agent(general-purpose) refactoring specialist (spawn 2) | Removal and verification | Safe removal + running the test suite and checking regressions (steps 4–5 combined) |
| MoAI orchestrator | Coordination | User approval, @MX tag cleanup (step 6, direct) |
You can revert with Git. MoAI removes in reverse dependency order and runs the tests, so if anything goes wrong it rolls back automatically.
Use it when you want to include the case where a function has exactly 1 caller and that caller is itself dead code. Useful for cleanup after large refactorings.
In --safe-only mode, only “certain dead code” is removed. Code used via reflection or dynamic dispatch is classified as a “false positive” and preserved.
InfoThe terminal CLImoai clean --homeshares the name but not the target — unlike/moai cleanabove (project dead code), this one tidies the~/.moaihome directory. It is not a slash command and does no dead-code analysis.
Session state, caches, logs, and stale profiles pile up in ~/.moai. moai clean --home tidies only the cleanup-target directories listed in its allowlist — anything not on the list stays, untouched and unasked. ~/.claude is never touched.
# Cleanup conversation — dry-run by default (report only, no deletion)
$ moai clean --home
# Real deletion — guarded force
$ moai clean --home --force- Dry-run is the default. Deletion requires an explicit
--force, and even that operates only inside the allowlist. - Before deleting, the Home Disk Usage check in
moai doctorreports how full things are — an advisory check whose thresholds follow the compiled defaults. - To relocate
~/.moaiitself, pointMOAI_HOMEat a new home root (only a non-empty absolute path is honored; an empty value equals unset and relative paths are disregarded). Note that only the Go binary reads this variable — shell hooks do not honor it.
| Category | Target | Condition |
|---|---|---|
debug | entries under claude-profiles/<profile>/debug/ | past the retention window |
releases | release binaries in releases/ (plus their paired .sha256) | everything except the current version and the 3 newest of the rest. version.json and LATEST are never candidates |
logs | files in the root logs/ | past the retention window |
backups | backups/removed-* directories | past the retention window |
Anything not on the list is invisible to the scanner. And the carve-outs (config/, state/, projects/, worktrees/, mcp/, bin/, search/, studio/, plugins/, plus launch.yaml, preferences.yaml, and every file whose name starts with credentials) win inside the allowlist too: if an aged backups/removed-* holds even one such file, the whole directory is skipped. ~/.claude is not read even under --force.
The retention window is read only from the HOME tier file ~/.moai/config/sections/state.yaml. It is a different key on a different tier from a project’s state.retention_days — there is one home but many projects, and separating the read site stops two projects from cleaning the same home with two different windows.
| Value | Behavior |
|---|---|
| Key absent / file absent | The default of 30 days |
| Positive integer | Only entries older than that many days become candidates |
0 | Cleaning disabled — no candidate is produced |
The full story: Home Directory Hygiene.