Claude CodeでPythonの\nが改行に化けた(unterminated string literal)。ヒアドキュメントをやめ、ファイルに書いてから実行した
2026年11月10日、部下役のAIにこのサイトの生成スクリプトを直してもらっていたら、Pythonが止まりました。
出たのは SyntaxError: unterminated string literal です。
文字列を閉じる引用符が見つからない、というエラーです。
書いたはずの \n(改行を表す2文字の記号)が、ファイルの中では本物の改行に変わっていました。
文字列が行の途中で切れたので、閉じていないと言われたわけです。
直し用のPythonを Write でファイルに保存してから動かすと、止まらずに通りました。
Write は、Claude Code がコマンドを通さずにファイルを書く機能です。
僕は Claude Code(AIにパソコンの作業を任せる道具)を、Windows で使っています。
コマンドを動かす Bash ツールの中身は、Git Bash です。
同じ止まり方は、9月4日と11月8日にも起きていました。
直したはずの1行が、6行に割れていた
11月10日に直していたのは、記事の抜粋(検索結果に出る説明文)を作る処理です。
部下役のAIは直し用の小さなPythonを組み、それに本体のファイルを書き換えさせました。
本体に入るはずだった行は、これです。
m = re.search(r"^# [^\n]+\n\n([^\n#<-][^\n]*)", md, re.M)
実際にファイルへ入ったのは、こうでした。
m = re.search(r"^# [^
]+
([^
#<-][^
]*)", md, re.M)
そのあと本体を動かすと、こう出て止まりました。
場所の前半は省いています。
File "…\build_posts.py", line 42
m = re.search(r"^# [^
^
SyntaxError: unterminated string literal (detected at line 42)
Pythonのふつうの文字列では、\n と書くと改行の1文字になります。
バックスラッシュと n の2文字のまま書き出すには、\\n と2本重ねて書きます。
直し用のPythonは、そう書いてありました。
それが1本に減って届いたので、改行になったのです。
同じ日にもう1本、直し用のPythonが止まっています。
書き換える場所を探す assert old in b(見つからなければ止める確認)が、AssertionError で落ちました。
探す文字列にも \\n が入っていたので、同じ理由で一致しなかったと見ています。
9月にも11月8日にも、同じエラーで止まっていた
最初は2026年9月4日です。
電子書籍の原稿を組み立てるスクリプトを直していて、2回止まりました。
このときは python - <<'PY' の形で、コマンドの中に書いたPythonをそのまま読ませていました。
<<'PY' から PY までの行をまとめて渡す書き方を、ヒアドキュメントといいます。
ファイルを作らずに、何行ものコードをコマンドへ直接書き込めます。
記録に残っているのは、return "\n" + "\n".join(rows) のつもりの行が、3行に割れて書き込まれた例です。
この日は、改行の文字を chr(10) で組み立てて直しました。chr(10) は、改行を文字の番号から作る書き方です。
そのあと、ファイルを書くときは cat > file.py <<'EOF' を使う、と作業メモに足しました。
cat は、受け取った文章をそのままファイルに書き出すコマンドです。
11月8日は、このサイトのCSS(見た目の指定)を合成するスクリプトでした。
やはり python - <<'PY' の形で、.rstrip("\n") の部分が改行に割れています。
止まったのは6行目で、エラーは同じ unterminated string literal でした。
このときはバックスラッシュを chr(92) で組み、構文を確かめてから動かしています。
作業メモと同じ cat の形で書いても化けた
11月10日の直し用のPythonは、作業メモと同じ cat > ファイル <<'PYEOF' の形で書いていました。
それでも化けたので、9月に足した手順では防げていなかったことになります。
そこで同じ日に、部下役のAIに手元で試してもらいました。
cat > h1.txt <<'EOF'
one:\n
two:\\n
EOF
od -c h1.txt
od -c は、ファイルの中身を1文字ずつ並べて見せるコマンドです。
結果はこうでした。
0000000 o n e : \ n \n t w o : \ n \n
1本の \n は、そのまま残っています。
2本の \\n は、1本に減っていました。
Pythonに直接読ませても同じです。
python - <<'PY'
print(repr('\\n'), len('\\n'))
PY
本来は '\\n' 2(2文字)と出るはずが、'\n' 1(改行の1文字)と出ました。
同じ1行を Write でファイルにして実行すると、'\\n' 2 と正しく出ています。
ヒアドキュメントに限った話でもありません。printf '%s' 'two:\\n' のように、単引用符で囲んだだけの文字でも1本に減りました。
9月4日に cat で確かめたときの中身は、\d や \n のような1本のバックスラッシュだけでした。
化けなかったのは、そのためだと見ています。
11月10日はPythonにファイルを書かせる形だったので、2本重ねた \\n が要りました。
試した環境の bash は、GNU bash 5.2.21(x86_64-pc-msys)です。
bashの決まりでは、引用符付きなら中身を変えない
bash の説明書(GNU Bash マニュアル)も開いて確かめました。
区切りの語に引用符が付いていれば、ヒアドキュメントの行は展開しない、と書かれています。<<'EOF' の形なら、中身は書いたとおりに渡るはずです。
GitHub の anthropics/claude-code には、同じ報告が出ていました。
Issue #90630 は、引用符付きのヒアドキュメントでもバックスラッシュが1段外れる、という報告です。
Issue #85856 は、Windows の Git Bash で Bash ツールのバックスラッシュが半分になる、という報告です。
Issue #85856 では、コマンドを bash に渡すときの書式の食い違いが原因と説明されています。
n本のバックスラッシュが、nを2で割って切り上げた本数で届く、とも書かれていました。
僕の環境で確かめたのは、2本が1本になるところまでです。
どちらのIssueも、僕が開いたときは閉じられていませんでした。
直し用のPythonは Write でファイルにしてから動かす
効いた手を並べると、こうなります。
- 直し用のPythonはコマンドの中に書かず、Write でファイルに保存してもらう
- そのファイルを
python fix.pyのように実行する - 書き換えた側のファイルは、
python -m py_compile 対象.pyで構文を確かめる
11月10日に効いたのは、1と2でした。
3の py_compile は、実行はせずに書き方の誤りだけを調べる仕組みです。
9月4日と11月8日は、これで直ったことを確かめてから動かしています。
この日は、直し用のPythonを作業用のフォルダに Write で置き、そこから一度に直しました。
書き込む行は r'...'(バックスラッシュを文字のまま扱う書き方)で書いています。
そのあと記事7本の組み立てと検査が、問題なしで通りました。
どうしてもコマンドの中で組むときは、バックスラッシュを文字の番号で作ります。
11月8日は、この形で直しました。
BS = chr(92) # バックスラッシュ1本
NL = chr(92) + "n" # バックスラッシュと n の2文字
すでにあるファイルの一部を書き換えるときは、探す文字列にも気をつけます。
バックスラッシュを含まない短い部分で探せば、この化け方の影響を受けません。assert が、合っているはずなのに落ちる。
そんなときは、先にここを疑うようにしています。
この記事の下書きを部下役のAIに頼んだときも、依頼文に一文を入れました。
Pythonはヒアドキュメントで書かず、Write でファイルにしてから実行する、という一文です。
Issue #90630 には、PowerShell 側の書き方や、/ で区切るパスで避ける方法も載っていました。
僕の環境では、そちらは試していません。
PowerShellのパスで \$ が $ になった、というメモもある
2026年10月10日には、Bash から PowerShell のスクリプトを書いていました。
作業メモには、"$root\$log" と書いたパスの \$ が $ になることがある、と残っています。
こうなると、フォルダ名とファイル名の間の区切りが消えてしまいます。
このときは、パスを Join-Path で組むように変えました。
Join-Path は、PowerShell でフォルダ名とファイル名をつなぐ命令です。
$log = Join-Path $root "logs\build.log"
この行は形を見せるためのもので、パスは実物と変えています。
ただ、実際に化けた行は、その日の会話の記録に見つかりませんでした。
その日のこの作業では、.ps1 をどれも引用符付きの <<'EOF' で書いています。
11月10日に同じ形で試すと、1本の \$ はそのまま残りました。
引用符の無い <<EOF なら、\$ は bash の決まりどおり $ になります。
マニュアルにも、その場合は \ が $ などの前で働く、とあります。
Join-Path に変えたあとも、スクリプトがログに書くはずの行は残りませんでした。
直ったのは、.ps1 の先頭にBOMを足したときです。
参考
- 運営記録 2026-09-04:電子書籍の組み立てスクリプトで2回停止・
chr(10)で修復 - 運営記録 2026-10-10:
\$が$になるというメモ・Join-Path に変更(化けた行は記録に無し) - 運営記録 2026-11-08:CSSの合成スクリプトで停止・
chr(92)と py_compile で修復 - 運営記録 2026-11-10:抜粋の処理で停止・Write で回避・手元での再現
- GNU Bash マニュアル「Here Documents」:https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Documents
- GitHub anthropics/claude-code Issue #90630:https://github.com/anthropics/claude-code/issues/90630
- GitHub anthropics/claude-code Issue #85856:https://github.com/anthropics/claude-code/issues/85856
コマンドに書いた文字が思わぬ読まれ方をして止まったことは、Codex CLI でもありました。
codex execに-iで画像を付けたら「No prompt provided via stdin」で止まった。後ろの指示文まで画像として読まれていた