I shipped my first wallpaper app in 2014 and have been building solo ever since. I am Masaki Hirokawa, an artist and creator who keeps a small indie app business (over fifty million cumulative downloads across several apps) running alongside my art practice.
Claude Code clearly speeds up code generation. But when one person maintains several apps, the generated code drifts a little from the rest of the codebase in subtle ways: indentation width, naming, import order, when to use guard versus if let. You notice it in a single file. Across ten or twenty files, it slowly erodes the feeling that "I wrote this." That bothered me.
Six weeks ago I wired SwiftLint into Claude Code's PostToolUse hook so that style checks run automatically the instant a file is edited. It is not a perfect system. But for keeping the codebase recognizably mine, it has been more effective than I expected. This is a record of those six weeks.
Why I bothered: keeping my codebase recognizably mine
Both of my grandfathers were miyadaiku, traditional shrine carpenters. On a miyadaiku site, every new piece of timber is checked for "alignment" with the existing pillars and beams. How nails are driven, the angle of a chamfer, the direction of the grain. Even details that no one will see in the finished structure have a standard. I want to keep the same feeling toward my code.
The Swift Claude Code outputs is fine as a baseline. But details like whether to omit return inside SwiftUI closures, the order of @State properties, or comment conventions vary in ways prompts cannot fully pin down. I used to fix this in manual review, but across several apps and dozens of edited files the review load piled up.
The plan was simple: run SwiftLint the instant Claude Code finishes editing a file, and let Claude itself fix the violations. A linter that only runs in CI is too slow. It has to run at the exact moment of the edit.
The minimal PostToolUse hook
Claude Code hooks let you run shell commands around tool calls. The goal here is to invoke SwiftLint right after Edit, Write, or MultiEdit.
Add a PostToolUse hook to ~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash $HOME/.claude/scripts/swiftlint-after-edit.sh"
}
]
}
]
}
}The matcher narrows the scope so the linter does not fire on Read or Grep. I started without a matcher, watched my thinking flow slow down, and quickly limited it to edit-class tools.
The script itself is minimal:
#!/usr/bin/env bash
# ~/.claude/scripts/swiftlint-after-edit.sh
set -euo pipefail
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
# Do nothing for non-Swift files
[[ "$FILE_PATH" == *.swift ]] || exit 0
# Walk up to find the nearest .swiftlint.yml
DIR="$(dirname "$FILE_PATH")"
while [[ "$DIR" != "/" ]]; do
[[ -f "$DIR/.swiftlint.yml" ]] && break
DIR="$(dirname "$DIR")"
done
# Skip projects with no .swiftlint.yml
[[ -f "$DIR/.swiftlint.yml" ]] || exit 0
cd "$DIR"
OUTPUT="$(swiftlint --quiet --path "$FILE_PATH" 2>&1 || true)"
if [[ -n "$OUTPUT" ]]; then
echo "SwiftLint findings for ${FILE_PATH}:"
echo "$OUTPUT"
fiThe jq line that extracts tool_input.file_path is quietly important. Hooks receive JSON on stdin; misread it and you silently get nothing forever. I lost the first few days to exactly that.
Three adjustments after six weeks of use
1. One .swiftlint.yml per app worked better than a shared one
My laptop has wallpaper apps and calm-themed apps side by side, and they style their UI differently. I started with a single shared .swiftlint.yml and discovered that what was correct in one codebase counted as a violation in the other.
I moved to a .swiftlint.yml at the root of each repository and let the script find the nearest one. Allowing per-project dialects was a better fit for real-world maintenance.
2. Stop running --fix from inside the hook
At first I ran swiftlint --fix --quiet --path "$FILE_PATH" to auto-correct in the hook. It was convenient, but rewriting a file while Claude Code is still editing it confuses Claude's diff awareness. One time the auto-fix collided with Claude's next edit and the same fix was applied twice.
Since then the hook only reports findings. Claude reads them on the next turn and decides whether and how to fix.
3. Treat the noisy first week as noise
In week one, every edit to legacy files surfaced a wall of pre-existing violations. Claude got confused and started reaching for files it should not have touched.
I narrowed .swiftlint.yml by adding excluded: paths and restricting only_rules::
only_rules:
- trailing_whitespace
- colon
- comma
- vertical_whitespace
- opening_brace
- control_statement
- identifier_name
- line_length
excluded:
- Pods
- Carthage
- Generated
- Legacy # ten-year-old code lives hereWhether to opt-in everything or start small is a matter of taste. I found starting small and adding trusted rules over time less stressful.
Four things that changed in six weeks
After those three adjustments the system felt stable. What I notice in daily use:
First, review time roughly halved. The style violations SwiftLint can see now mostly vanish inside the same Claude turn, freeing my eyes for design judgment and test coverage.
Second, the style of Claude Code's output converges within a session. When the hook returns a violation, Claude fixes it on the next turn, and that learning sticks for the rest of the session. I no longer have to explain in prose that "we omit return in this repo" — the linter teaches it backwards.
Third, the psychological cost of touching legacy files dropped. Even in code that mixes Objective-C from a decade ago, the hook tells Claude mechanically which lines to fix, so I can comfortably ask for "the smallest diff that resolves the SwiftLint findings only."
Fourth, slightly surprisingly, commit messages got more specific. Because violations are explicit, Claude writes messages like "Fix four trailing_whitespace and two identifier_name issues."
Rough edges I hit
To be honest, here are the rough edges I have lived with.
swiftlint's startup overhead is noticeable when fired per file. On an M1 Mac each invocation is a few hundred milliseconds, but right after a MultiEdit touches ten files at once the tempo drops visibly. --quiet, narrower only_rules:, and broader excluded: all help, but as long as the linter runs on every edit it will not be zero.
The other one is that long hook stdout slowly chews through Claude Code's context. I capped each file's findings to roughly ten lines with a trailing head -20. That was enough.
What I want to try next
The same shape generalizes to any linter or type checker. I am about to do the same thing in my Python scripts using ruff.
Whenever I think about what I want to leave for my children who live apart from me, I am reminded why it is worth writing the technical details down clearly. Even with a capable partner like Claude Code, whether the codebase still feels like mine depends on how I design the running surface around it. A thin mechanism like Hooks, run long enough, makes that difference visible.
If you are also a one-person shop bouncing between repositories, I hope some of this is useful. Thank you for reading.