autocorrect.zsh — 167줄짜리 명령어 자동 정정 zsh 스크립트
autocorrect.zsh는 명령어 실행이 실패하면 Gemini Flash로 자동 정정 명령어를 제안하는 167줄짜리 zsh 스크립트입니다. jq와 Gemini API 키만 있으면 새 터미널 앱 없이 기존 zsh에서 바로 동작합니다. 이 글에서는 동작 원리, 핵심 설계, 사용법, 한계를 정리합니다.
무엇이 다른가
Warp는 AI 명령어 보정 기능을 터미널 앱 수준에서 제공합니다. 기능 자체는 유용하지만, 기존 zsh 환경을 유지하면서 그 기능만 필요한 경우에는 전체 셸 환경을 교체하는 트레이드오프가 맞지 않습니다. autocorrect.zsh는 그 차이를 겨냥합니다. 기존 iTerm2나 터미널닷앱을 그대로 두고 .zshrc에 source 한 줄만 추가하면 동일한 자동 정정 흐름이 동작합니다. Gemini Flash 기준 API 호출 비용은 건당 거의 0원입니다.
동작 데모
명령어 실패 후 AI 제안, 피드백 반영, 재수정까지의 흐름입니다.
$ du -h depth 1 top 10
du: depth: No such file or directory
[AUTO] Failed (exit 1). Fix with AI? (ENTER/n):
[WAIT] ...
[INFO] du -hd 1 | sort -hr | head -n 10
ENTER to execute / f to give feedback / any key to cancel: 최신순
[WAIT] ...
[INFO] du -h -d 1 . | sort -hr | head -n 10
ENTER to execute / f to give feedback / any key to cancel:
213G .
195G ./GitHub
8.4G ./Telegram Desktop한국어 자연어 입력도 처리됩니다. zsh가 한국어 문자열을 명령어로 해석하려다 exit 127을 내뱉는 순간, 훅이 그 실패를 잡아서 AI에 넘깁니다.
$ 이름에 'cc'가 포함된 폴더를 찾아줘. 하위 폴더는 뒤지지 마.
zsh: command not found: 이름에
[AUTO] Failed (exit 127). Fix with AI? (ENTER/n):
[WAIT] ...
[INFO] ls -d *cc*
ENTER to execute / f to give feedback / any key to cancel:
analyze-cc-prompts
cc_teammates_test
cc-project결과적으로 자연어 입력이 shell 명령어로 변환됩니다.
전체 소스
#──────────────────────────────────────────────────────────────────────────────
# Command Auto Correction System (Gemini Flash)
#──────────────────────────────────────────────────────────────────────────────
# 자동 감지 + fix 수동 호출 두 가지 모드 지원
# - 자동 감지: 커맨드 실패 시 제안
# - 수동: fix "command" 또는 fix 인터랙티브
# 백업: claude_autocorrect.zsh.bak (Claude Haiku 버전)
# 환경정보 (셸 시작 시 1회 수집)
_FIX_OS="$(uname -s)"
[[ -f /proc/version ]] && grep -qi microsoft /proc/version 2>/dev/null && _FIX_OS="WSL"
_FIX_SHELL_NAME="${SHELL##*/}"
if [[ -n "$ZSH_VERSION" ]]; then
_FIX_SHELL="${_FIX_SHELL_NAME} ${ZSH_VERSION}"
elif [[ -n "$BASH_VERSION" ]]; then
_FIX_SHELL="${_FIX_SHELL_NAME} ${BASH_VERSION}"
else
_FIX_SHELL="$_FIX_SHELL_NAME"
fi
_FIX_ARCH="$(uname -m)"
_FIX_ENV="${_FIX_OS}/${_FIX_ARCH} | ${_FIX_SHELL}"
_FIX_MODEL="${GEMINI_MODEL:-gemini-3.5-flash}"
# Gemini Flash API 호출로 커맨드 검증/정정
_gemini_cmd_fixer() {
local user_input="$*"
source ~/.oh-my-zsh/custom/apikey.env 2>/dev/null
if [[ -z "$GEMINI_API_KEY" ]]; then
echo "[ERROR] GEMINI_API_KEY not found"
return 1
fi
local prompt="[env: $_FIX_ENV] [cwd: $(pwd)]
$user_input
-> 사용자 의도를 정확히 반영한 올바른 명령어 한 줄만. 규칙: 1) 사용자가 원하는 결과를 정확히 달성할 것 2) 위 env 정보에 맞는 옵션만 사용 (OS/셸별 차이 주의. 예: macOS coreutils는 GNU와 다름, BSD sort는 -h 미지원, bash와 zsh 문법 차이 등) 3) 사용자가 요청하지 않은 무거운 작업을 추가하지 말 것. 반드시 {\"cmd\": \"명령어\"} JSON 형식으로 응답."
local json_prompt=$(printf '%s' "$prompt" | jq -Rs '.')
echo "[WAIT] ..."
local raw=$(curl -s --connect-timeout 5 \
"https://generativelanguage.googleapis.com/v1beta/models/${_FIX_MODEL}:generateContent?key=${GEMINI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"contents": [{"parts": [{"text": '"$json_prompt"'}]}],
"generationConfig": {
"maxOutputTokens": 100,
"thinkingConfig": {"thinkingLevel": "minimal"},
"responseMimeType": "application/json",
"responseJsonSchema": {
"type": "object",
"properties": {
"cmd": {"type": "string"}
},
"required": ["cmd"]
}
}
}' 2>/dev/null)
if [[ -z "$raw" ]]; then
echo "[ERROR] No response (network/timeout)"
return 1
fi
if printf '%s' "$raw" | grep -q '"error"'; then
echo "[ERROR] $(printf '%s' "$raw" | jq -r '.error.message // .error.status // "unknown"' 2>/dev/null)"
return 1
fi
# structured output → JSON {"cmd": "..."} 파싱
# NOTE: zsh의 echo는 \\ → \ 변환하므로 반드시 printf 사용
local suggested_cmd=$(printf '%s' "$raw" | jq -r '.candidates[0].content.parts[-1].text' 2>/dev/null | jq -r '.cmd // empty' 2>/dev/null)
if [[ -z "$suggested_cmd" ]]; then
echo "[ERROR] Parse failed"
return 1
fi
echo ""
echo "[INFO] $suggested_cmd"
echo ""
read -r "?ENTER to execute / f to give feedback / any key to cancel: " confirm
if [[ -z "$confirm" ]]; then
local output
output=$(eval "$suggested_cmd" 2>&1)
local eval_status=$?
local pipe_failed=0
for s in ${pipestatus[@]}; do
[[ $s -ne 0 ]] && pipe_failed=1
done
printf '%s\n' "$output"
echo ""
# exit code 실패 또는 파이프 중간 실패 또는 usage/error 패턴 감지
if [[ $eval_status -ne 0 || $pipe_failed -eq 1 ]] || printf '%s' "$output" | grep -qiE '^usage:|: (command not found|not found|no such file|invalid option|illegal option)'; then
echo ""
read -r "?[RETRY] Failed. ENTER to retry / feedback to guide / any key to cancel: " retry
if [[ -z "$retry" ]]; then
_gemini_cmd_fixer "$suggested_cmd (failed: exit $eval_status, error: $output)"
elif [[ "$retry" != [nNqQ] ]]; then
_gemini_cmd_fixer "$suggested_cmd ($retry)"
fi
fi
elif [[ "$confirm" == [fF] || ${#confirm} -gt 1 ]]; then
local feedback="$confirm"
[[ "$confirm" == [fF] ]] && read -r "?Feedback: " feedback
_gemini_cmd_fixer "$user_input ($feedback)"
else
echo "[CANCEL]"
return 1
fi
}
# ========== 자동 감지 모드 ==========
_auto_fix_last_command() {
local exit_status=$?
local last_cmd="$1"
[[ $exit_status -eq 0 || $exit_status -eq 130 ]] && return 0
local cmd_first="${last_cmd%% *}"
[[ "$cmd_first" =~ ^(vim|nano|subl|code|less|man|fix|cmd_fix)$ ]] && return 0
echo ""
read -r -k 1 "?[AUTO] Failed (exit $exit_status). Fix with AI? (ENTER/n): " response
echo ""
if [[ "$response" == "y" || "$response" == "Y" || "$response" == "" || "$response" == $'\n' ]]; then
_gemini_cmd_fixer "$last_cmd"
fi
}
# precmd hook 등록 (자동 감지)
if [[ -o interactive ]]; then
autoload -U add-zsh-hook 2>/dev/null
_store_last_command() {
LAST_COMMAND="$1"
}
_check_and_fix() {
_auto_fix_last_command "$LAST_COMMAND"
}
add-zsh-hook preexec _store_last_command 2>/dev/null
add-zsh-hook precmd _check_and_fix 2>/dev/null
fi
# ========== 수동 호출 모드 ==========
cmd_fix() {
local user_input
if [[ $# -eq 0 ]]; then
read -r -p "Enter command or natural language: " user_input
else
user_input="$*"
fi
if [[ -z "$user_input" ]]; then
echo "No input provided"
return 1
fi
_gemini_cmd_fixer "$user_input"
}
alias fix='cmd_fix'구조화된 출력으로 파싱 신뢰성 확보
Gemini Flash를 호출할 때 responseJsonSchema를 명시해서 응답 형식을 고정합니다.
"responseJsonSchema": {
"type": "object",
"properties": {
"cmd": {"type": "string"}
},
"required": ["cmd"]
}모델이 {"cmd": "du -hd 1 | sort -hr | head -n 10"} 형태로만 응답하도록 강제합니다. 자유 텍스트로 받으면 설명 문장, 마크다운 코드 블록, 줄바꿈이 섞이는 경우가 생기고 파싱 로직이 복잡해집니다. 구조화된 출력을 쓰면 jq -r '.cmd' 한 줄로 처리가 끝납니다. API 응답을 다룰 때는 zsh의 echo 대신 printf를 써야 합니다. zsh의 echo는 백슬래시를 자동 변환하기 때문입니다.
preexec + precmd 훅 구조
자동 감지 모드는 zsh 훅 두 개의 조합으로 동작합니다.
add-zsh-hook preexec _store_last_command
add-zsh-hook precmd _check_and_fixpreexec는 명령어가 실행되기 직전에 호출됩니다. 여기서 명령어 문자열을 LAST_COMMAND 변수에 저장합니다. precmd는 프롬프트가 표시되기 직전, 즉 명령어 실행이 끝난 뒤에 호출됩니다. 여기서 $?로 exit code를 읽어 0이 아니면 AI 제안 프롬프트를 띄웁니다.
두 훅을 분리하는 이유는 precmd 시점에서는 이미 exit code가 갱신되기 시작하기 때문입니다. 명령어 문자열을 preexec 시점에 미리 저장하지 않으면 어떤 명령어가 실패했는지 알 방법이 없습니다. exit 130(Ctrl-C)은 정상 중단으로 간주해서 무시하고, vim, man, less 같은 인터랙티브 명령어도 자동 제안 대상에서 제외합니다.
재시도 루프
제안된 명령어가 실행 후 또 실패하면 재시도 흐름이 시작됩니다.
[RETRY] Failed. ENTER to retry / feedback to guide / any key to cancel:실행된 명령어의 출력을 캡처해서 exit code 비정상, pipe 실패, usage: / command not found 패턴 중 하나라도 감지되면 다시 묻습니다. ENTER를 누르면 실패한 명령어를 그대로 다시 AI에 보내고, 텍스트를 입력하면 그 피드백을 붙여서 재호출합니다. _gemini_cmd_fixer가 재귀 구조이므로 납득할 때까지 루프가 계속됩니다. 실패 출력이 컨텍스트로 누적되기 때문에 두 번째 제안이 더 정확해지는 경향이 있습니다. BSD/GNU coreutils 차이로 인한 실패도 이 루프로 자연스럽게 처리됩니다.
한계
API 호출이 포함되므로 응답까지 약 1초 지연이 있습니다. 네트워크 상태에 따라 더 걸릴 수 있고, 오프라인에서는 동작하지 않습니다.
GEMINI_API_KEY는 ~/.oh-my-zsh/custom/apikey.env에 평문으로 저장됩니다. 키 노출 위험이 있으므로 파일 권한을 chmod 600으로 설정하는 것이 좋습니다.
OS, arch, shell 버전 정보를 매 호출마다 프롬프트에 주입해서 macOS BSD와 GNU coreutils 차이를 LLM이 처리하도록 설계했습니다. 대부분의 경우에는 정확하게 동작하지만, 플래그 추론은 LLM에 의존하므로 100% 보장은 아닙니다.
설치
소스는 github.com/jaesolshin/autocorrect에 있습니다.
# 설치
git clone https://github.com/jaesolshin/autocorrect
echo 'source /path/to/autocorrect.zsh' >> ~/.zshrc
# API 키 설정
echo 'GEMINI_API_KEY="your-key-here"' >> ~/.oh-my-zsh/custom/apikey.env
chmod 600 ~/.oh-my-zsh/custom/apikey.env