📝 Originally published (in Japanese) at forge.workstyle.tech.
TL;DR The Stop hook reported Unterminated string because printf with %b emitted a raw newline inside a JSON string. The hook also used a cd path fixed to an earlier environment. Its change count missed staged and untracked files when it checked only git diff --name-only.
TL;DR
The Stop hook error Hook output looks like a JSON object but is not valid JSON — JSON Parse error: Unterminated string. occurred because the printf command’s %b format specifier emitted a raw newline inside a JSON string. I changed the hook to build JSON with Python’s json.dumps function, which converts values into JSON-formatted strings, and also reviewed where it runs and how Git changes are counted.
Why Did the Stop Hook JSON Report “Unterminated string”?
The direct cause of the JSON error was that printf’s %b format specifier output the newlines in the string as-is.
The error was:
Hook output looks like a JSON object but is not valid JSON — JSON Parse error: Unterminated string.
printf is a command for writing output such as strings. When you use %b in its format, any newlines in the string are reflected in the output. As a result, a raw newline ended up inside a JSON string, making it look as though the string had been cut off. Unterminated string means that a string was never closed.
At first, I thought I had made a mistake in the JSON structure somewhere in the code that reports changes. The error message told me the output was malformed, but not which value caused it, so I checked the code that builds the JSON.
Is Your Stop Hook Still Running in the Previous Environment?
The issue I found beyond the JSON was that the cd at the start of the process still pointed to a location from the previous environment.
Even when that directory change failed, the process continued. To produce the correct output, you also need to check where the hook is running commands. I changed it to determine the location at runtime instead of hardcoding it.
What Was Missing When Counting Git Changes
I was only checking git diff --name-only, so staged changes and untracked files were left out.
Staged changes are changes added to Git for a commit. Untracked files are files Git has not yet recognized as being under version control. Since the goal is to notify about uncommitted changes, a method that picks up only some of them results in incomplete notifications.
I changed the count to use git status --porcelain=v1 -z. Fixing the JSON error alone would not solve the issue of missing changes in the count.
How I Fixed the JSON and Change Notifications
I revised three things: where the script runs, how it checks for changes, and how it builds JSON.
First, I removed the cd command that hardcoded the previous environment’s location and made the script determine its location at runtime. Next, I counted changes with git status --porcelain=v1 -z and included staged and untracked files in the check. Finally, I stopped assembling JSON manually with the shell’s printf and used Python’s json.dumps instead.
Does the Output Remain Valid JSON When Filenames Contain Newlines and Other Special Characters?
After the fix, I checked the output with filenames containing quotation marks, backslashes, newlines, Japanese characters, and spaces.
Before the fix, the output was malformed. Afterward, it was correct. Producing a string that looks like JSON isn’t enough; I also verified that the output is valid JSON for filenames containing the characters that caused the issue.
We Don’t Know When the Stop Hook Broke
Because the configuration was outside Git and had no change history, I couldn’t determine when it broke.
The causes I confirmed this time were raw newlines in a JSON string, an outdated execution location, and an incomplete set of changes being counted. The hook failed without stopping the work, and I only checked after seeing an error. The fact that work continues does not prove that the hook is working correctly.
Key takeaways
- Build JSON with Python's
json.dumpsinstead of shellprintf. - Resolve the working directory at runtime instead of keeping an old fixed
cdpath. - Use
git status --porcelain=v1 -zto count staged and untracked files too. - Check output with filenames containing quotes, backslashes, newlines, Japanese text, and spaces.
- The configuration had no change history, so when the hook broke is unknown.
Top comments (0)