Skip to content

feat(zed): add known_human checkpoint extension#1048

Open
svarlamov wants to merge 8 commits intomainfrom
feat/known-human-zed
Open

feat(zed): add known_human checkpoint extension#1048
svarlamov wants to merge 8 commits intomainfrom
feat/known-human-zed

Conversation

@svarlamov
Copy link
Copy Markdown
Member

@svarlamov svarlamov commented Apr 11, 2026

Summary

  • New Zed extension (agent-support/zed/) that fires git-ai checkpoint known_human --hook-input stdin on file save with 500ms debounce per repo root
  • New ZedInstaller (src/mdm/agents/zed.rs) that auto-installs the extension source, the hook wrapper script, and configures format_on_save in Zed's settings.json
  • Uses include_str! to embed all extension source files at compile time

Extension API approach

Finding: The Zed WASM extension API (v0.5–v0.7) has no file-save callbacks.

The Extension trait exposes only: language server hooks, debug adapter hooks, slash commands, context servers, and docs indexing. There is no on_save, on_buffer_change, or equivalent. See: https://docs.rs/zed_extension_api/0.7.0/zed_extension_api/trait.Extension.html

Approach used: format_on_save external command

Zed exposes a "formatter": { "external": { "command": "...", "arguments": [] } } setting that fires on every file save and receives the full file content via stdin. The ZedInstaller writes a wrapper shell script (git-ai-zed-hook.sh) and injects the format_on_save configuration into ~/.config/zed/settings.json (Linux) or ~/Library/Application Support/Zed/settings.json (macOS).

The wrapper script:

  1. Captures stdin (file content) and emits it unchanged to stdout (formatter contract)
  2. Runs git -C <dir> rev-parse --show-toplevel to find the repo root
  3. Fires git-ai checkpoint known_human --hook-input stdin in the background with a 500ms debounce per repo root

The WASM extension stub is still installed in the Zed extensions directory so Zed loads and displays a "git-ai" extension entry.

Manual install

# Copy extension source
cp -r agent-support/zed ~/.config/zed/extensions/installed/git-ai

# Install hook script
install -m755 agent-support/zed/git-ai-zed-hook.sh ~/.config/zed/git-ai-zed-hook.sh

# Add to ~/.config/zed/settings.json:
# "formatter": { "external": { "command": "~/.config/zed/git-ai-zed-hook.sh", "arguments": [] } },
# "format_on_save": "on"

Then restart Zed.

Test plan

  • cargo clippy -- -D warnings passes
  • cargo test passes (all unit tests for ZedInstaller pass)
  • Manual: install with git-ai mdm install, save a file in Zed, verify git-ai checkpoint known_human fires

🤖 Generated with Claude Code


Open with Devin

Adds a Zed extension that fires git-ai checkpoint known_human on file save
with 500ms debounce per repo root, plus a ZedInstaller that auto-installs
the extension source and format_on_save hook to the Zed config directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +25 to +30
CONTENT="$(cat)"

# ------------------------------------------------------------------
# 2. Emit content unchanged (the formatter contract).
# ------------------------------------------------------------------
printf '%s' "$CONTENT"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bash command substitution strips trailing newlines, silently modifying every saved file

CONTENT="$(cat)" at line 25 uses bash command substitution, which always strips trailing newlines from the captured output. When the content is re-emitted at line 30 with printf '%s' "$CONTENT", any trailing newline(s) from the original file are lost. Since this script acts as Zed's format_on_save formatter, every file save will silently remove the file's trailing newline — causing Zed to see a diff and apply the modification. Most text files (and POSIX convention) end with a trailing newline, so this would alter virtually every file on every save.

Standard fix for preserving trailing newlines in bash
CONTENT="$(cat; printf x)"
CONTENT="${CONTENT%x}"

This appends a sentinel character before the command substitution strips newlines, then removes it, preserving the original trailing newlines.

Suggested change
CONTENT="$(cat)"
# ------------------------------------------------------------------
# 2. Emit content unchanged (the formatter contract).
# ------------------------------------------------------------------
printf '%s' "$CONTENT"
CONTENT="$(cat; printf x)"
CONTENT="${CONTENT%x}"
# ------------------------------------------------------------------
# 2. Emit content unchanged (the formatter contract).
# ------------------------------------------------------------------
printf '%s' "$CONTENT"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

mkdir -p "$LOCK_DIR"

# Hash the repo root path to create a safe filename
REPO_HASH="$(printf '%s' "$REPO_ROOT" | sha256sum | cut -c1-16 2>/dev/null || printf '%s' "$REPO_ROOT" | md5sum | cut -c1-16)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 sha256sum and md5sum unavailable on stock macOS causes script to exit non-zero, breaking file saves

Line 70 tries sha256sum then falls back to md5sum, but neither command exists on stock macOS (which ships shasum and md5 instead). With set -euo pipefail (line 20), when both pipelines fail, the command substitution exits non-zero, causing the script to terminate with an error. Since the script's own comment at line 7 states "Non-zero exit causes Zed to show an error and discard stdout", this means file saves would fail on macOS — Zed's primary platform. The project's own install.sh:110-113 handles this correctly by falling back to shasum -a 256.

Suggested change
REPO_HASH="$(printf '%s' "$REPO_ROOT" | sha256sum | cut -c1-16 2>/dev/null || printf '%s' "$REPO_ROOT" | md5sum | cut -c1-16)"
REPO_HASH="$(printf '%s' "$REPO_ROOT" | sha256sum 2>/dev/null | cut -c1-16 || printf '%s' "$REPO_ROOT" | shasum -a 256 2>/dev/null | cut -c1-16 || printf '%s' "$REPO_ROOT" | md5sum 2>/dev/null | cut -c1-16 || printf '%s' "$REPO_ROOT" | md5 2>/dev/null | cut -c1-16 || printf '%s' "$REPO_ROOT" | cksum | cut -d' ' -f1)"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +276 to +287
let mut j = i + 1;
while j < n
&& (lines[j].trim() == "],"
|| lines[j].trim() == "]"
|| lines[j].trim() == "},"
|| lines[j].trim() == "}"
|| lines[j].contains("\"arguments\"")
|| lines[j].contains("\"format_on_save\""))
{
j += 1;
}
skip_count = j - i - 1;
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 remove_formatter_block skip-ahead loop consumes the top-level closing } of settings.json

The forward-skip loop at lines 284-293 greedily skips any line whose trimmed content equals } or },. After removing the formatter block's own closing braces, the loop continues and also matches the top-level JSON closing } (e.g., the final } of settings.json). This causes the uninstall to produce invalid JSON missing its closing brace.

Trace through test case at zed.rs:579-595

Given settings content:

{
  "other_setting": true,
  "formatter": { ... our block ... },
  "format_on_save": "on"
}

When the script-path line (index 4) is matched, the skip-ahead loop starting at j=5 walks through "arguments", }, },, "format_on_save", and then also } (the top-level closing brace at index 9) because lines[9].trim() == "}" is true. The result is {\n "other_setting": true,\n — invalid JSON missing its closing brace. The existing test only asserts the removed content is gone and other_setting is present, so it passes despite the corruption.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

svarlamov and others added 7 commits April 11, 2026 18:10
…ection, and fix fragile uninstall

- Escape backslashes and double-quotes in file path and repo root when python3 is unavailable in the fallback path of git-ai-zed-hook.sh
- Skip formatter injection in install_settings if a \"formatter\" key already exists in settings.json to avoid invalid JSON with duplicate keys
- Restrict remove_formatter_block to only pop lines containing \"external\" or matching the exact \"formatter\" key prefix, preventing accidental removal of unrelated keys like \"default_formatter\"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment on lines +178 to +183
// Find the last `}` and insert before it.
let trimmed = original.trim_end_matches(['\n', '\r']);
if let Some(pos) = trimmed.rfind('}') {
let (before, _after) = trimmed.split_at(pos);
let before = before.trim_end_matches(',');
format!("{before},\n{formatter_snippet}\n}}\n")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 install_settings produces invalid JSON when settings.json is an empty object {}

When Zed's settings.json contains an empty JSON object (e.g. {} or {\n}), the install_settings method at src/mdm/agents/zed.rs:175-183 enters the else branch, finds the last }, splits before it, then unconditionally formats as "{before},\n{snippet}\n}}". For an empty object, before is "{" (or "{\n"), producing {,\n "formatter"... which is invalid JSON — a comma immediately after the opening brace with no preceding entry.

This is a realistic scenario because new Zed installations often have settings.json with just {} or {\n}. The corrupted settings file would prevent Zed from loading its configuration.

Trace for settings.json = "{}":
  1. original.trim().is_empty() → false, original.contains('{') → true → enters else branch
  2. trimmed = "{}", rfind('}') = 1
  3. before = "{", after trimming commas still "{"
  4. Result: "{,\n \"formatter\": ...\n}\n" → invalid JSONC
Suggested change
// Find the last `}` and insert before it.
let trimmed = original.trim_end_matches(['\n', '\r']);
if let Some(pos) = trimmed.rfind('}') {
let (before, _after) = trimmed.split_at(pos);
let before = before.trim_end_matches(',');
format!("{before},\n{formatter_snippet}\n}}\n")
// Find the last `}` and insert before it.
let trimmed = original.trim_end_matches(['\n', '\r']);
if let Some(pos) = trimmed.rfind('}') {
let (before, _after) = trimmed.split_at(pos);
let before_trimmed = before.trim_end_matches(',');
// If the object is empty (before is just `{` plus whitespace), don't add comma
if before_trimmed.trim_end().ends_with('{') {
format!("{before_trimmed}\n{formatter_snippet}\n}}\n")
} else {
format!("{before_trimmed},\n{formatter_snippet}\n}}\n")
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant