2025/08/10 の時点の自分用のメモです。
Windows環境だからなのかハマった。その時のメモを残します
- Claude Code Hooks とは
- 環境について
- Hooks を入れたいと思ったこと
- hooksの設定テストについて
- 試した設定
- Windows特有の対応方法(失敗した内容)
- stdinの内容をJSONを出力させる
- 最終形の設定
- 参考 URL
Claude Code Hooks とは
Claude Code Hooks は、Claude Code の動作を決定論的に制御する強力なシステムです。特定の条件下でのみコマンドを実行する。 ログを残したり、フォーマッターを実行したり、自動でテストしたり、終了通知をしたりできます。
環境について
- 自分がおこなっているのは Windows 環境で WSL の環境です。
- powershell でも Claude Code は起動できる
インストールしたもの
- Node.js
- git2.3+
- JQ
- uvx
Hooks を入れたいと思ったこと
私が触っっているプログラムの文字コードが shift-jis にだった。 Claude Code は utf-8 で動いているので文字コードの問題に直面しました。 Claude Code に指示をしても文字化けが解消されることはなかった。
下記を対応することで文字化けを対応しようと思いました。
hooksの設定テストについて
hooks の設定ファイルは変更したあとに再読込しないといけなかったので コマンドで修正する形で対応した
claude --dangerously-skip-permissions --debug -p "update hooktest.XXX 上部のコメントをtest1に変更する"
--dangerously-skip-permissions はテストなので何も OK にした --debug はログ出力しないとまったくわからなかったため追加した -p が claude code の画面を出さずに内容を実行してくれるモード
試した設定
.claude\settings.local.json を作成(自分の場合はもともとあったので変更)した
{ "permissions": { "allow": ["Bash(cp:*)", "Bash(rm:*)", "Bash(sed:*)"], "deny": [] }, "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "python3 convert_encoding_utf8_to_shiftjis.py \"$CLAUDE_FILE_PATHS\"" } ] } ], "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "python3 convert_encoding_shiftjis_to_utf8.py \"$CLAUDE_FILE_PATHS\"" } ] } ] } }
変更される予定のファイルと変更されたファイルを渡すことで文字コードを変換するようにしたかった
これがうまくいかなかった
CLAUDE_FILE_PATHS
という環境変数が何をしても取得できなかった。
参考にしたサイトだと以下でCLAUDE_FILE_PATHSが出力された。
{ "hooks": { "PostToolUse": [ { "name": "File Change Notification", "condition": { "tool_name": "edit_file" }, "hooks": [ { "type": "command", "command": "echo '📝 File modified: $CLAUDE_FILE_PATHS'" } ] } ] } }
どうしても出力されなかった。
本家の Claude の AI の聞いたら Windows 環境だと違うじゃないの? 出力された内容は以下だった
Windows特有の対応方法(失敗した内容)
失敗した内容なのでみたい人だけどうぞ
ここをクリックすると失敗の履歴
1. PowerShell を使用した方法
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "powershell -Command \"$input = [Console]::In.ReadToEnd(); Write-Host $input\"" } ] } ] } }
2. 外部スクリプトファイルを使用(推奨)
hook_handler.ps1 ファイルを作成:
# hook_handler.ps1
$input = [Console]::In.ReadToEnd()
$jsonData = $input | ConvertFrom-Json
# JSONデータを整形して出力
Write-Host "=== Hook Input JSON ==="
Write-Host ($jsonData | ConvertTo-Json -Depth 10)
# 特定のフィールドにアクセス
if ($jsonData.tool_input -and $jsonData.tool_input.file_path) {
Write-Host "File Path: $($jsonData.tool_input.file_path)"
}
# 環境変数も確認
Write-Host "=== Environment Variables ==="
Get-ChildItem env: | Where-Object Name -like '*CLAUDE*' | ForEach-Object {
Write-Host "$($_.Name): $($_.Value)"
}
フック設定:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File hook_handler.ps1" } ] } ] } }
3. Python スクリプトを使用
hook_handler.py ファイルを作成:
import sys import json def main(): try: # stdinからJSONデータを読み取り input_data = sys.stdin.read() print("=== Raw Input ===") print(input_data) if input_data.strip(): # JSONとして解析 json_data = json.loads(input_data) print("\n=== Parsed JSON ===") print(json.dumps(json_data, indent=2, ensure_ascii=False)) # 特定のフィールドにアクセス if 'tool_input' in json_data: print(f"\nTool Input: {json_data['tool_input']}") if 'session' in json_data: print(f"\nSession Info: {json_data['session']}") else: print("No input received") except json.JSONDecodeError as e: print(f"JSON decode error: {e}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": main()
フック設定:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "python hook_handler.py" } ] } ] } }
4. バッチファイルを使用
hook_handler.bat ファイルを作成:
@echo off echo === Hook Input === set /p input= echo %input% echo === End Input ===
フック設定:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "hook_handler.bat" } ] } ] } }
どれも動かず・・・途方にくれていました
stdinの内容をJSONを出力させる
公式のドキュメントに
フックは、セッション情報とイベント固有のデータを含む JSON データを stdin 経由で受信します
と記載があったのでそちらをもとに 検索したら以下のページがヒットした
{ "hooks": { "PostToolUse": [{ "matcher": "Task|TodoWrite|Write|MultiEdit|Edit", "hooks": [{ "type": "command", "command": "jq '.' >> ~/.claude/post_tool_use_hook_debug.log" }] }] } }
上記の設定では、Claude CodeがTaskやWrite等のToolを使用した後にjqコマンドが実行され、コマンドの結果が~/.claude/post_tool_use_hook_debug.log に出力されます。jqコマンドは標準入力として入力されたJSON文字列を解析(パース)し、整形して出力します。jq '.' の 「.」 は、「入力されたJSONをそのまま出力する」という意味のフィルタになります。
実際に実行したら自分の環境でも成功した 微妙に違うがほぼこの内容だった 実際に出力されたログ
{
"session_id": "6da5a0c0-1d49-44ce-b3b4-9c0c627c33ad",
"transcript_path": "/home/ubuntu/.claude/projects/-mnt-d---------svn------------------/6da5a0c0-1d49-44ce-b3b4-9c0c627c33ad.jsonl",
"cwd": "/mnt/d/作業用フォルダ/svn/コンテンツ/作業用/テスト",
"hook_event_name": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "/mnt/d/作業用フォルダ/svn/コンテンツ/作業用/テスト/hooktest.asp",
"old_string": "<%\nOption Explicit",
"new_string": "<%\n' test19\nOption Explicit"
}
}
環境変数にいれたいものが tool_input.file_path に入ることがわかりました
最終形の設定
{ "permissions": { "allow": ["Bash(cp:*)", "Bash(rm:*)", "Bash(sed:*)"], "deny": [] }, "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq '.' > .claude/hooks/post_tool_json_data.log && python3 .claude/hooks/convert_encoding_utf8_to_shiftjis.py .claude/hooks/post_tool_json_data.log" } ] } ], "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq '.' > .claude/hooks/pre_tool_json_data.log && python3 .claude/hooks/convert_encoding_shiftjis_to_utf8.py .claude/hooks/pre_tool_json_data.log" } ] } ] } }
いらないかもしれないけどJSONを解析してファイルパスを取得して その後文字コード変換するプログラムです
・convert_encoding_shiftjis_to_utf8.py
みたい人はここをクリックすると見れます
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ UTF-8変換ツール(バックアップなし直接保存版) Shift-JISからUTF-8への変換 """ import sys import os import codecs import chardet import tempfile import shutil import errno import json class Config: MAX_FILE_SIZE = 100 * 1024 * 1024 CHUNK_SIZE = 65536 DETECTION_SAMPLE_SIZE = 65536 ENCODING_SHIFT_JIS = 'SHIFT_JIS' ENCODING_UTF8 = 'UTF-8' ENCODING_UTF8_SIG_BOM = 'UTF-8-SIG(BOMあり)' DEFAULT_READ_ENCODING_FOR_ASCII = 'utf-8' def is_binary_file(filepath): try: with open(filepath, 'rb') as f: chunk = f.read(1024) if not chunk: return False if chunk.startswith(b'\xff\xfe') or chunk.startswith(b'\xfe\xff'): return False return b'\x00' in chunk except Exception: return True def has_bom_utf8(filepath): try: with open(filepath, 'rb') as f: bom = f.read(3) return bom == b'\xef\xbb\xbf' except Exception: return False def normalize_encoding_name(encoding): if not encoding: return encoding encoding_lower = encoding.lower() mapping = { 'utf-8': Config.ENCODING_UTF8, 'utf8': Config.ENCODING_UTF8, 'utf-8-sig': Config.ENCODING_UTF8_SIG_BOM, 'utf8-sig': Config.ENCODING_UTF8_SIG_BOM, 'utf-16le': 'UTF-16LE', 'utf16le': 'UTF-16LE', 'utf-16be': 'UTF-16BE', 'utf16be': 'UTF-16BE', 'euc-jp': 'EUC-JP', 'eucjp': 'EUC-JP', 'iso-2022-jp': 'ISO-2022-JP', 'iso2022jp': 'ISO-2022-JP', 'shift_jis': Config.ENCODING_SHIFT_JIS, 'shift-jis': Config.ENCODING_SHIFT_JIS, 'sjis': Config.ENCODING_SHIFT_JIS, 'cp932': Config.ENCODING_SHIFT_JIS, 'windows-31j': Config.ENCODING_SHIFT_JIS, 'windows-1252': 'WINDOWS-1252', 'iso-8859-1': 'ISO-8859-1', } if encoding_lower in mapping: return mapping[encoding_lower] return encoding.upper() def test_utf8_decode(raw_data): try: raw_data.decode('utf-8') return True except UnicodeDecodeError: return False def detect_encoding(filepath): try: file_size = os.path.getsize(filepath) if file_size > Config.MAX_FILE_SIZE: return None, f"ファイルサイズが大きすぎます({file_size // (1024*1024)}MB)" if file_size == 0: return None, "空ファイルです" sample_size = min(file_size, Config.DETECTION_SAMPLE_SIZE) with open(filepath, 'rb') as f: raw_data = f.read(sample_size) if not raw_data: return None, "空ファイルです" if raw_data.startswith(b'\xff\xfe'): return 'UTF-16LE', None elif raw_data.startswith(b'\xfe\xff'): return 'UTF-16BE', None has_utf8_bom = raw_data.startswith(b'\xef\xbb\xbf') if has_utf8_bom: if test_utf8_decode(raw_data[3:]): return Config.ENCODING_UTF8_SIG_BOM, None else: return None, "UTF-8 BOMがありますが、内容が不正です" if is_binary_file(filepath): return None, "バイナリファイルです" data_for_chardet = raw_data[3:] if has_utf8_bom else raw_data result = chardet.detect(data_for_chardet) if result is None or result.get('encoding') is None: return None, "文字コードを検出できませんでした" detected_chardet_encoding = result['encoding'] confidence = result.get('confidence', 0) normalized_encoding = normalize_encoding_name(detected_chardet_encoding) if normalized_encoding in [Config.ENCODING_UTF8, 'ASCII']: if test_utf8_decode(raw_data): return Config.ENCODING_UTF8, None if confidence > 0.8: if normalized_encoding == Config.ENCODING_SHIFT_JIS and test_utf8_decode(raw_data): return Config.ENCODING_UTF8, None return normalized_encoding, None return None, f"文字コードの検出信頼性が低いです ({detected_chardet_encoding}, confidence={confidence:.2f})" except PermissionError: return None, "ファイルにアクセスできません" except Exception as e: return None, f"エンコーディング検出エラー: {str(e)}" def get_read_encoding(encoding): if encoding == Config.ENCODING_UTF8_SIG_BOM: return 'utf-8-sig' elif encoding == Config.ENCODING_UTF8: return 'utf-8' elif encoding == Config.ENCODING_SHIFT_JIS: return 'cp932' elif encoding == 'EUC-JP': return 'euc-jp' elif encoding == 'ISO-2022-JP': return 'iso-2022-jp' elif encoding == 'WINDOWS-1252': return 'windows-1252' elif encoding == 'ISO-8859-1': return 'iso-8859-1' elif encoding == 'ASCII': return Config.DEFAULT_READ_ENCODING_FOR_ASCII else: return encoding.lower().replace('-', '_') def create_temp_file_safely(output_dir): temp_fd = -1 temp_filepath = None try: if not os.path.isdir(output_dir): try: os.makedirs(output_dir, exist_ok=True) except OSError as e: raise Exception(f"一時ファイル用ディレクトリの作成に失敗: {output_dir}, {str(e)}") temp_fd, temp_filepath = tempfile.mkstemp( suffix='.tmp', prefix='utf8_conv_', dir=output_dir ) os.close(temp_fd) temp_fd = -1 return temp_filepath except Exception as e: if temp_fd != -1: try: os.close(temp_fd) except OSError: pass if temp_filepath and os.path.exists(temp_filepath): try: os.remove(temp_filepath) except OSError: pass raise Exception(f"一時ファイルの作成に失敗: {str(e)}") def convert_file_stream(input_filepath, encoding): """ファイルを直接上書き変換(バックアップなし)""" read_encoding = get_read_encoding(encoding) temp_file = None try: output_dir = os.path.dirname(input_filepath) if not output_dir: output_dir = os.getcwd() # 一時ファイルを作成 temp_file = create_temp_file_safely(output_dir) write_encoding = 'utf-8' # 一時ファイルにUTF-8で書き込み with codecs.open(input_filepath, 'r', encoding=read_encoding, errors='replace') as infile, \ codecs.open(temp_file, 'w', encoding=write_encoding, errors='replace') as outfile: while True: chunk = infile.read(Config.CHUNK_SIZE) if not chunk: break outfile.write(chunk) # 一時ファイルを元ファイルに上書き(バックアップなし) try: shutil.move(temp_file, input_filepath) temp_file = None # 移動成功したのでNoneにしてfinallyでの削除を回避 except OSError as e: if hasattr(e, 'errno') and e.errno == errno.EEXIST: raise Exception(f"Output file {input_filepath} already exists.") raise Exception(f"Failed to move temporary file: {str(e)}") return True, None except PermissionError: return False, 'ファイルの保存権限がありません' except OSError as e: if hasattr(e, 'winerror') and e.winerror == 112: return False, 'ディスク容量が不足しています' if e.errno == errno.ENOSPC: return False, 'ディスク容量が不足しています' return False, f'ファイルの保存/OSエラー: {str(e)}' except Exception as e: return False, f'ファイルの保存中に予期せぬエラー: {str(e)}' finally: # 一時ファイルが残っている場合は削除 if temp_file and os.path.exists(temp_file): try: os.remove(temp_file) except OSError: pass def convert_to_utf8(filepath): """メイン変換関数(直接上書き版)""" result = { 'success': False, 'message': '', 'original_encoding': '', 'converted': False, 'skipped': False } try: if not os.path.exists(filepath): result['message'] = 'ファイルが見つかりません' return result abs_filepath = os.path.abspath(filepath) if not os.path.isfile(abs_filepath): result['message'] = '有効なファイルパスではありません' return result encoding, error_msg = detect_encoding(abs_filepath) if error_msg: if error_msg == "空ファイルです": result['success'] = True result['message'] = '空ファイルのためスキップ' result['skipped'] = True result['original_encoding'] = 'N/A' return result else: result['message'] = error_msg return result if encoding is None: result['message'] = '文字コードを検出できませんでした' return result result['original_encoding'] = encoding # UTF-8やUTF-8 BOMの場合はスキップ if encoding in [Config.ENCODING_UTF8, Config.ENCODING_UTF8_SIG_BOM]: result['success'] = True result['message'] = 'UTF-8のためスキップ' result['skipped'] = True return result # 直接上書き変換 conversion_success, conversion_error = convert_file_stream(abs_filepath, encoding) if conversion_success: result['success'] = True result['message'] = f'{os.path.basename(abs_filepath)} を直接上書き変換' result['converted'] = True else: result['message'] = conversion_error if conversion_error else 'ファイル変換に失敗しました' return result except KeyboardInterrupt: result['message'] = 'ユーザーによりキャンセルされました' return result except Exception as e: result['message'] = f'予期しないエラー: {str(e)}' return result def format_result_message(result, original_filename): if not result['success'] and not result['skipped']: return f"{original_filename} → 変換失敗 ({result['message']})" if result['skipped']: return f"{original_filename} → {result['message']}" base_message = f"{original_filename} ({result['original_encoding']}) → 直接上書き変換完了 ({Config.ENCODING_UTF8})" return base_message def _display_help_message(): """使用方法のメッセージを表示する.""" print("=" * 30) print("UTF-8変換ツール(JSON入力版)") print("=" * 30) print("使用方法:") print("1. JSONファイルのtool_input.file_pathで指定されたファイルの文字コードをUTF-8に変換します。") print(" ⚠️ 注意:バックアップは作成されません!") print(" 対応文字コード: Shift-JIS、EUC-JP、ISO-2022-JPなど。") print(" 対応ファイル: テキストベースのファイル (例: csv, txt, html, asp)。バイナリファイルは不可。") print(f" 制限: 最大ファイルサイズ {Config.MAX_FILE_SIZE // (1024*1024)}MB。") print("2. 使用方法: python3 program.py json_file.json") print("3. JSONファイル形式:") print(" {") print(" \"tool_input\": {") print(" \"file_path\": \"/path/to/target/file\"") print(" }") print(" }") print("4. UTF-8は多くの文字を表現できるため、文字化けは基本的に発生しません。") def _process_files_from_args(filepaths): """コマンドライン引数で渡されたファイルを処理し、結果を返す.""" results = [] converted_count = 0 skipped_count = 0 failed_count = 0 total_count = len(filepaths) print("変換処理を開始します...") print("⚠️ 元ファイルを直接上書きします(バックアップなし)\n") for i, filepath_arg in enumerate(filepaths, 1): try: original_filename = os.path.basename(filepath_arg) if filepath_arg else f"ファイル{i}" print(f"処理中 ({i}/{total_count}): {original_filename}") result = convert_to_utf8(filepath_arg) results.append((original_filename, result)) if result['success']: if result.get('skipped', False): skipped_count += 1 elif result.get('converted', False): converted_count += 1 else: if not result.get('skipped', False): failed_count +=1 except Exception as e: print(f"エラーが発生しました: {e}", file=sys.stderr) failed_count += 1 return results, converted_count, skipped_count, failed_count, total_count def _display_conversion_summary(results, converted_count, skipped_count, failed_count, total_count): """変換結果の要約と詳細を表示する.""" print(f"\n{'='*50}") print("=== 変換結果 ===") for fname, res_item in results: print(format_result_message(res_item, fname)) summary_parts = [] if converted_count > 0: summary_parts.append(f"{converted_count}ファイル変換") if skipped_count > 0: summary_parts.append(f"{skipped_count}ファイルスキップ") if failed_count > 0: summary_parts.append(f"{failed_count}ファイル失敗") summary_str = "、".join(summary_parts) if summary_parts else "処理対象なし" print(f"\n処理完了({total_count}ファイル中 {summary_str})") print(f"{'='*50}") def main(): try: if len(sys.argv) > 1: json_filepath = sys.argv[1] # JSONファイルの存在確認 if not os.path.exists(json_filepath): print(f"エラー: JSONファイルが見つかりません: {json_filepath}") return # JSONファイルを読み込み try: with open(json_filepath, 'r', encoding='utf-8') as f: json_data = json.load(f) # tool_input.file_pathを取得 if 'tool_input' in json_data and 'file_path' in json_data['tool_input']: file_path = json_data['tool_input']['file_path'] filepaths = [file_path] # 配列として設定 print(f"JSONから取得したファイルパス: {file_path}") else: print("エラー: JSONファイルにtool_input.file_pathが見つかりません") return except json.JSONDecodeError as e: print(f"エラー: JSONファイルの解析に失敗しました: {e}") return except Exception as e: print(f"エラー: JSONファイルの読み込みに失敗しました: {e}") return results, converted_count, skipped_count, failed_count, total_count = _process_files_from_args(filepaths) _display_conversion_summary(results, converted_count, skipped_count, failed_count, total_count) else: _display_help_message() except KeyboardInterrupt: print("\nプログラムが中断されました。") except Exception as e: import traceback print(f"\n重大なエラーが発生しました: {str(e)}", file=sys.stderr) print(f"{traceback.format_exc()}", file=sys.stderr) except (EOFError, KeyboardInterrupt): pass if __name__ == "__main__": main()
・convert_encoding_utf8_to_shiftjis.py
みたい人はここをクリックすると見れます
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Shift-JIS変換ツール(バックアップなし直接保存版) """ import sys import os import codecs import chardet import tempfile import shutil import errno import json class Config: MAX_FILE_SIZE = 100 * 1024 * 1024 CHUNK_SIZE = 65536 DETECTION_SAMPLE_SIZE = 65536 COMPATIBILITY_CHECK_SIZE = 1024 ENCODING_SHIFT_JIS = 'SHIFT_JIS' ENCODING_UTF8 = 'UTF-8' ENCODING_UTF8_SIG_BOM = 'UTF-8-SIG(BOMあり)' DEFAULT_READ_ENCODING_FOR_ASCII = 'utf-8' def is_binary_file(filepath): try: with open(filepath, 'rb') as f: chunk = f.read(1024) if not chunk: return False if chunk.startswith(b'\xff\xfe') or chunk.startswith(b'\xfe\xff'): return False return b'\x00' in chunk except Exception: return True def has_bom_utf8(filepath): try: with open(filepath, 'rb') as f: bom = f.read(3) return bom == b'\xef\xbb\xbf' except Exception: return False def normalize_encoding_name(encoding): if not encoding: return encoding encoding_lower = encoding.lower() mapping = { 'utf-8': Config.ENCODING_UTF8, 'utf8': Config.ENCODING_UTF8, 'utf-8-sig': Config.ENCODING_UTF8_SIG_BOM, 'utf8-sig': Config.ENCODING_UTF8_SIG_BOM, 'utf-16le': 'UTF-16LE', 'utf16le': 'UTF-16LE', 'utf-16be': 'UTF-16BE', 'utf16be': 'UTF-16BE', 'euc-jp': 'EUC-JP', 'eucjp': 'EUC-JP', 'iso-2022-jp': 'ISO-2022-JP', 'iso2022jp': 'ISO-2022-JP', 'shift_jis': Config.ENCODING_SHIFT_JIS, 'shift-jis': Config.ENCODING_SHIFT_JIS, 'sjis': Config.ENCODING_SHIFT_JIS, 'cp932': Config.ENCODING_SHIFT_JIS, 'windows-31j': Config.ENCODING_SHIFT_JIS, 'windows-1252': 'WINDOWS-1252', 'iso-8859-1': 'ISO-8859-1', } if encoding_lower in mapping: return mapping[encoding_lower] return encoding.upper() def test_utf8_decode(raw_data): try: raw_data.decode('utf-8') return True except UnicodeDecodeError: return False def detect_encoding(filepath): try: file_size = os.path.getsize(filepath) if file_size > Config.MAX_FILE_SIZE: return None, f"ファイルサイズが大きすぎます({file_size // (1024*1024)}MB)" if file_size == 0: return None, "空ファイルです" sample_size = min(file_size, Config.DETECTION_SAMPLE_SIZE) with open(filepath, 'rb') as f: raw_data = f.read(sample_size) if not raw_data: return None, "空ファイルです" if raw_data.startswith(b'\xff\xfe'): return 'UTF-16LE', None elif raw_data.startswith(b'\xfe\xff'): return 'UTF-16BE', None has_utf8_bom = raw_data.startswith(b'\xef\xbb\xbf') if has_utf8_bom: if test_utf8_decode(raw_data[3:]): return Config.ENCODING_UTF8_SIG_BOM, None else: return None, "UTF-8 BOMがありますが、内容が不正です" if is_binary_file(filepath): return None, "バイナリファイルです" data_for_chardet = raw_data[3:] if has_utf8_bom else raw_data result = chardet.detect(data_for_chardet) if result is None or result.get('encoding') is None: return None, "文字コードを検出できませんでした" detected_chardet_encoding = result['encoding'] confidence = result.get('confidence', 0) normalized_encoding = normalize_encoding_name(detected_chardet_encoding) if normalized_encoding in [Config.ENCODING_UTF8, 'ASCII']: if test_utf8_decode(raw_data): return Config.ENCODING_UTF8, None if confidence > 0.8: if normalized_encoding == Config.ENCODING_SHIFT_JIS and test_utf8_decode(raw_data): return Config.ENCODING_UTF8, None return normalized_encoding, None return None, f"文字コードの検出信頼性が低いです ({detected_chardet_encoding}, confidence={confidence:.2f})" except PermissionError: return None, "ファイルにアクセスできません" except Exception as e: return None, f"エンコーディング検出エラー: {str(e)}" def get_read_encoding(encoding): if encoding == Config.ENCODING_UTF8_SIG_BOM: return 'utf-8-sig' elif encoding == Config.ENCODING_UTF8: return 'utf-8' elif encoding == Config.ENCODING_SHIFT_JIS: return 'cp932' elif encoding == 'EUC-JP': return 'euc-jp' elif encoding == 'ISO-2022-JP': return 'iso-2022-jp' elif encoding == 'WINDOWS-1252': return 'windows-1252' elif encoding == 'ISO-8859-1': return 'iso-8859-1' elif encoding == 'ASCII': return Config.DEFAULT_READ_ENCODING_FOR_ASCII else: return encoding.lower().replace('-', '_') def check_char_sjis_compatibility(char): try: encoded = char.encode('shift_jis') decoded = encoded.decode('shift_jis') return char == decoded except (UnicodeEncodeError, UnicodeDecodeError): return False def check_sjis_compatibility_stream(filepath, encoding): try: read_encoding = get_read_encoding(encoding) incompatible_found = False with codecs.open(filepath, 'r', encoding=read_encoding, errors='replace') as infile: while True: chunk = infile.read(Config.COMPATIBILITY_CHECK_SIZE) if not chunk: break for char in chunk: if not check_char_sjis_compatibility(char): incompatible_found = True break if incompatible_found: break return incompatible_found except Exception: return True def create_temp_file_safely(output_dir): temp_fd = -1 temp_filepath = None try: if not os.path.isdir(output_dir): try: os.makedirs(output_dir, exist_ok=True) except OSError as e: raise Exception(f"一時ファイル用ディレクトリの作成に失敗: {output_dir}, {str(e)}") temp_fd, temp_filepath = tempfile.mkstemp( suffix='.tmp', prefix='sjis_conv_', dir=output_dir ) os.close(temp_fd) temp_fd = -1 return temp_filepath except Exception as e: if temp_fd != -1: try: os.close(temp_fd) except OSError: pass if temp_filepath and os.path.exists(temp_filepath): try: os.remove(temp_filepath) except OSError: pass raise Exception(f"一時ファイルの作成に失敗: {str(e)}") def convert_file_stream(input_filepath, encoding): """ファイルを直接上書き変換(バックアップなし)""" read_encoding = get_read_encoding(encoding) temp_file = None try: output_dir = os.path.dirname(input_filepath) if not output_dir: output_dir = os.getcwd() # 一時ファイルを作成 temp_file = create_temp_file_safely(output_dir) write_encoding = 'shift_jis' # 一時ファイルにShift-JISで書き込み with codecs.open(input_filepath, 'r', encoding=read_encoding, errors='replace') as infile, \ codecs.open(temp_file, 'w', encoding=write_encoding, errors='replace') as outfile: while True: chunk = infile.read(Config.CHUNK_SIZE) if not chunk: break outfile.write(chunk) # 一時ファイルを元ファイルに上書き(バックアップなし) try: shutil.move(temp_file, input_filepath) temp_file = None # 移動成功したのでNoneにしてfinallyでの削除を回避 except OSError as e: if hasattr(e, 'errno') and e.errno == errno.EEXIST: raise Exception(f"Output file {input_filepath} already exists.") raise Exception(f"Failed to move temporary file: {str(e)}") return True, None except PermissionError: return False, 'ファイルの保存権限がありません' except OSError as e: if hasattr(e, 'winerror') and e.winerror == 112: return False, 'ディスク容量が不足しています' if e.errno == errno.ENOSPC: return False, 'ディスク容量が不足しています' return False, f'ファイルの保存/OSエラー: {str(e)}' except Exception as e: return False, f'ファイルの保存中に予期せぬエラー: {str(e)}' finally: # 一時ファイルが残っている場合は削除 if temp_file and os.path.exists(temp_file): try: os.remove(temp_file) except OSError: pass def convert_to_sjis(filepath): """メイン変換関数(直接上書き版)""" result = { 'success': False, 'message': '', 'original_encoding': '', 'has_incompatible_chars': False, 'converted': False, 'skipped': False } try: if not os.path.exists(filepath): result['message'] = f'ファイルが見つかりません: {filepath}' return result abs_filepath = os.path.abspath(filepath) if not os.path.isfile(abs_filepath): result['message'] = '有効なファイルパスではありません' return result encoding, error_msg = detect_encoding(abs_filepath) if error_msg: if error_msg == "空ファイルです": result['success'] = True result['message'] = '空ファイルのためスキップ' result['skipped'] = True result['original_encoding'] = 'N/A' return result else: result['message'] = error_msg return result if encoding is None: result['message'] = '文字コードを検出できませんでした' return result result['original_encoding'] = encoding if encoding == Config.ENCODING_SHIFT_JIS: result['success'] = True result['message'] = 'SHIFT_JISのためスキップ' result['skipped'] = True return result result['has_incompatible_chars'] = check_sjis_compatibility_stream(abs_filepath, encoding) # 直接上書き変換 conversion_success, conversion_error = convert_file_stream(abs_filepath, encoding) if conversion_success: result['success'] = True result['message'] = f'{os.path.basename(abs_filepath)} を直接上書き変換' result['converted'] = True else: result['message'] = conversion_error if conversion_error else 'ファイル変換に失敗しました' return result except KeyboardInterrupt: result['message'] = 'ユーザーによりキャンセルされました' return result except Exception as e: result['message'] = f'予期しないエラー: {str(e)}' return result def format_result_message(result, original_filename): if not result['success'] and not result['skipped']: return f"{original_filename} → 変換失敗 ({result['message']})" if result['skipped']: return f"{original_filename} → {result['message']}" base_message = f"{original_filename} ({result['original_encoding']}) → 直接上書き変換完了 ({Config.ENCODING_SHIFT_JIS})" if result['has_incompatible_chars']: base_message += "、代替文字に置換あり" return base_message def _display_help_message(): """使用方法のメッセージを表示する.""" print("=" * 30) print("Shift_JIS変換ツール(JSON入力版)") print("=" * 30) print("使用方法:") print("1. JSONファイルのtool_input.file_pathで指定されたファイルの文字コードをShift_JISに変換します。") print(" ⚠️ 注意:バックアップは作成されません!") print(" 対応文字コード: UTF-8、EUC-JP、ISO-2022-JPなど。") print(" 対応ファイル: テキストベースのファイル (例: csv, txt, html, asp)。バイナリファイルは不可。") print(f" 制限: 最大ファイルサイズ {Config.MAX_FILE_SIZE // (1024*1024)}MB。") print("2. 使用方法: python3 program.py json_file.json") print("3. JSONファイル形式:") print(" {") print(" \"tool_input\": {") print(" \"file_path\": \"/path/to/target/file\"") print(" }") print(" }") print("4. Shift_JISで表現できない文字は代替文字[?]に置換されます。") def _process_files_from_args(filepaths): """コマンドライン引数で渡されたファイルを処理し、結果を返す.""" results = [] converted_count = 0 skipped_count = 0 failed_count = 0 total_count = len(filepaths) print("変換処理を開始します...") print(f"対象ファイル: {filepaths}") print("⚠️ 元ファイルを直接上書きします(バックアップなし)\n") for i, filepath_arg in enumerate(filepaths, 1): try: original_filename = os.path.basename(filepath_arg) if filepath_arg else f"ファイル{i}" print(f"処理中 ({i}/{total_count}): {original_filename}") result = convert_to_sjis(filepath_arg) results.append((original_filename, result)) if result['success']: if result.get('skipped', False): skipped_count += 1 elif result.get('converted', False): converted_count += 1 else: if not result.get('skipped', False): failed_count +=1 except Exception as e: print(f"エラーが発生しました: {e}", file=sys.stderr) failed_count += 1 return results, converted_count, skipped_count, failed_count, total_count def _display_conversion_summary(results, converted_count, skipped_count, failed_count, total_count): """変換結果の要約と詳細を表示する.""" print(f"\n{'='*50}") print("=== 変換結果 ===") for fname, res_item in results: print(format_result_message(res_item, fname)) summary_parts = [] if converted_count > 0: summary_parts.append(f"{converted_count}ファイル変換") if skipped_count > 0: summary_parts.append(f"{skipped_count}ファイルスキップ") if failed_count > 0: summary_parts.append(f"{failed_count}ファイル失敗") summary_str = "、".join(summary_parts) if summary_parts else "処理対象なし" print(f"\n処理完了({total_count}ファイル中 {summary_str})") print(f"{'='*50}") def main(): try: if len(sys.argv) > 1: json_filepath = sys.argv[1] # JSONファイルの存在確認 if not os.path.exists(json_filepath): print(f"エラー: JSONファイルが見つかりません: {json_filepath}") return # JSONファイルを読み込み try: with open(json_filepath, 'r', encoding='utf-8') as f: json_data = json.load(f) # tool_input.file_pathを取得 if 'tool_input' in json_data and 'file_path' in json_data['tool_input']: file_path = json_data['tool_input']['file_path'] filepaths = [file_path] # 配列として設定 print(f"JSONから取得したファイルパス: {file_path}") else: print("エラー: JSONファイルにtool_input.file_pathが見つかりません") return except json.JSONDecodeError as e: print(f"エラー: JSONファイルの解析に失敗しました: {e}") return except Exception as e: print(f"エラー: JSONファイルの読み込みに失敗しました: {e}") return results, converted_count, skipped_count, failed_count, total_count = _process_files_from_args(filepaths) _display_conversion_summary(results, converted_count, skipped_count, failed_count, total_count) else: _display_help_message() except KeyboardInterrupt: print("\nプログラムが中断されました。") except Exception as e: import traceback print(f"\n重大なエラーが発生しました: {str(e)}", file=sys.stderr) print(f"{traceback.format_exc()}", file=sys.stderr) except (EOFError, KeyboardInterrupt): pass if __name__ == "__main__": main()
参考 URL
本家
https://docs.anthropic.com/en/docs/claude-code/hooks-guide
hooks がまとまったサイト
https://smartscope.blog/AI/claude-code-hooks-advanced/
最終的に助けてくれたサイト
https://dev.classmethod.jp/articles/claude-code-hooks-basic-usage/