feat: structured RPC for hook scripts (#47, phase 2)#54
Merged
Conversation
Replaces the string-interpolated "build Lua source in bash" pattern with
JSON args dispatched through a single Lua entry point. User-controlled
data (file paths, file contents) flows as JSON through a temp file and is
decoded by vim.json.decode inside nvim — no user data ever enters a Lua
source string, so the entire escape_lua quoting layer is gone.
New:
- bin/nvim-call.sh — nvim_call(mod, fn, json_args) helper. Only mod/fn
and the temp-file path (all our own values) get interpolated into Lua
source. Returns rc=0/1/2 (ok / no socket / dispatch failed).
- lua/code-preview/rpc.lua — dispatch(mod, fn, args_file) reads JSON,
validates it's an array, calls require(mod)[fn](unpack(args)). Errors
are surfaced via log.error (vim.notify) so silent dispatch failures
are visible to the developer.
Removed:
- bin/nvim-send.sh — escape_lua and the dead nvim_send function.
Migrated all 13 callsites:
- core-pre-tool.sh: changes.set, neo_tree.refresh/reveal, hook_context,
diff.show_diff (Edit/Write/MultiEdit + ApplyPatch paths).
- core-post-tool.sh: diff.close_for_file, changes.clear_by_statuses.
- backends/{copilot,codex}/code-{preview,close}-diff.sh: swapped the
six inline `vim.json.encode({debug=...})` queries for a single
code-preview.log.state() RPC.
Supporting:
- log.state() — bundled {debug, log_file, servername, cwd} so backends
get all logging-setup fields in one round trip.
- neo_tree.refresh_deferred / reveal_deferred — so callers don't build
vim.defer_fn(...) as a string.
- health.lua: swapped nvim-send.sh for nvim-call.sh in the executable
check; added a load check for code-preview.rpc.
Bug fixes uncovered during testing (both pre-existing, both around bash
quoting brittleness that this migration was designed to retire):
- detect_rm_paths didn't strip outer quotes, so `rm "/abs/path"`
resolved to $CWD/"/abs/path". Now strips surrounding ' and " and
handles ~/ via a quoted case pattern.
- xargs trim on RM_PATHS / WRITE_PATHS does shell-style quote
processing — a single apostrophe in a filename (e.g. it's-mine.txt)
silently zeroed the list. Replaced with parameter-expansion trim.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2 of #47 — replaces the string-interpolated "build Lua source in bash" pattern with structured JSON args dispatched through a single Lua entry point.
What changes
Before: Bash scripts built Lua source code as strings (
\"require('...').show_diff('\$ESC_PATH', ...)\") and sent them vianvim --remote-expr. Anescape_luased helper escaped every interpolated arg to avoid syntax errors when user data contained apostrophes, backslashes, or newlines. Easy to forget; one mistake = silent hook failure or quote breakout.After: Bash builds a JSON args array with
jq(which handles escaping correctly), writes it to a temp file, and callsnvim_call MOD FN JSON_ARGS. Inside nvim,code-preview.rpc.dispatchdecodes the JSON and callsrequire(MOD)[FN](unpack(args))with real Lua values. No user data ever enters a Lua source string — only the module name, function name, and temp-file path (all our own values) get interpolated.Files
New
Removed
Migrated (13 callsites)
Supporting Lua helpers
Pre-existing bug fixes uncovered during testing
Both are exactly the category of bash-quoting brittleness this migration was designed to retire — found them because Test 3 (filenames with apostrophes) finally exercised the unhappy paths:
Tested manually
Independent review
Asked a separate Claude Code session to audit the diff. Critical findings (silent dispatch failures, missing JSON-array validation, missing health check, undocumented field-shape contract) are all addressed in this PR. Reviewer's lower-priority findings (cosmetic / pre-existing / out-of-scope) deferred to follow-ups.
Out of scope
Test plan
🤖 Generated with Claude Code