Skip to content

Commit 0f7bf83

Browse files
fix(plugin): reduce Claude prompt hook latency
1 parent 3a92f50 commit 0f7bf83

3 files changed

Lines changed: 69 additions & 3 deletions

File tree

internal/setup/setup_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,6 +1802,67 @@ func TestClaudeCodeUserPromptHookUsesCurrentMCPServerID(t *testing.T) {
18021802
}
18031803
}
18041804

1805+
func TestClaudeCodeUserPromptHookDefersProjectDetectionUntilNeeded(t *testing.T) {
1806+
data, err := os.ReadFile(filepath.Join("..", "..", "plugin", "claude-code", "scripts", "user-prompt-submit.sh"))
1807+
if err != nil {
1808+
t.Fatalf("read user prompt hook: %v", err)
1809+
}
1810+
text := string(data)
1811+
1812+
sessionParse := strings.Index(text, "SESSION_ID=$(echo \"$INPUT\" | jq -r '.session_id // empty')")
1813+
sessionKeyBranch := strings.Index(text, "if [ -n \"$SESSION_ID\" ]; then")
1814+
if sessionParse < 0 || sessionKeyBranch < 0 {
1815+
t.Fatalf("user prompt hook missing expected session parsing/keying structure")
1816+
}
1817+
if preKey := text[sessionParse:sessionKeyBranch]; strings.Contains(preKey, "detect_project") {
1818+
t.Fatalf("user prompt hook must not detect project before session_id-first keying")
1819+
}
1820+
1821+
fallbackDetect := "PROJECT=$(detect_project \"$CWD\")\n SAFE_PROJECT="
1822+
if !strings.Contains(text, fallbackDetect) {
1823+
t.Fatalf("user prompt hook should detect project only for the no-session_id fallback key")
1824+
}
1825+
1826+
subsequentMarker := strings.Index(text, "# SUBSEQUENT MESSAGES")
1827+
if subsequentMarker < 0 {
1828+
t.Fatalf("user prompt hook missing subsequent-message section")
1829+
}
1830+
if !strings.Contains(text[subsequentMarker:], "PROJECT=$(detect_project \"$CWD\")") {
1831+
t.Fatalf("user prompt hook should detect project for subsequent nudge logic after first-message handling")
1832+
}
1833+
}
1834+
1835+
func TestClaudeCodeUserPromptSubmitHookTimeout(t *testing.T) {
1836+
data, err := os.ReadFile(filepath.Join("..", "..", "plugin", "claude-code", "hooks", "hooks.json"))
1837+
if err != nil {
1838+
t.Fatalf("read Claude Code hooks config: %v", err)
1839+
}
1840+
1841+
var cfg struct {
1842+
Hooks map[string][]struct {
1843+
Hooks []struct {
1844+
Command string `json:"command"`
1845+
Timeout int `json:"timeout"`
1846+
} `json:"hooks"`
1847+
} `json:"hooks"`
1848+
}
1849+
if err := json.Unmarshal(data, &cfg); err != nil {
1850+
t.Fatalf("parse Claude Code hooks config: %v", err)
1851+
}
1852+
1853+
entries := cfg.Hooks["UserPromptSubmit"]
1854+
if len(entries) != 1 || len(entries[0].Hooks) != 1 {
1855+
t.Fatalf("expected one UserPromptSubmit command hook, got %#v", entries)
1856+
}
1857+
hook := entries[0].Hooks[0]
1858+
if hook.Command != "${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-submit.sh" {
1859+
t.Fatalf("unexpected UserPromptSubmit command %q", hook.Command)
1860+
}
1861+
if hook.Timeout != 5 {
1862+
t.Fatalf("UserPromptSubmit timeout = %d, want 5", hook.Timeout)
1863+
}
1864+
}
1865+
18051866
func TestAddClaudeCodeAllowlist(t *testing.T) {
18061867
t.Run("creates file from scratch", func(t *testing.T) {
18071868
resetSetupSeams(t)

plugin/claude-code/hooks/hooks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
{
3232
"type": "command",
3333
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-submit.sh",
34-
"timeout": 2
34+
"timeout": 5
3535
}
3636
]
3737
}

plugin/claude-code/scripts/user-prompt-submit.sh

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ source "${SCRIPT_DIR}/_helpers.sh"
2121
INPUT=$(cat)
2222
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
2323
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
24-
PROJECT=$(detect_project "$CWD")
2524

2625
parse_epoch() {
2726
TS="$1"
@@ -74,7 +73,8 @@ OUTPUT="{}"
7473
if [ -n "$SESSION_ID" ]; then
7574
SESSION_KEY="engram-claude-${SESSION_ID}-tools-loaded"
7675
else
77-
# No session ID available — key on project to avoid repeated injections
76+
# No session ID available — only then detect project for the fallback state key.
77+
PROJECT=$(detect_project "$CWD")
7878
SAFE_PROJECT=$(printf '%s' "${PROJECT:-unknown}" | tr -cs 'a-zA-Z0-9_-' '_')
7979
SESSION_KEY="engram-claude-${SAFE_PROJECT}-$$-tools-loaded"
8080
fi
@@ -99,6 +99,11 @@ fi
9999
# SUBSEQUENT MESSAGES — existing save-nudge logic
100100
# ──────────────────────────────────────────────────────────────────────────────
101101

102+
# Detect project only after the first-message path has had a chance to return.
103+
if [ -z "${PROJECT:-}" ]; then
104+
PROJECT=$(detect_project "$CWD")
105+
fi
106+
102107
# Bail early if we can't determine the project
103108
if [ -z "$PROJECT" ]; then
104109
echo "$OUTPUT"

0 commit comments

Comments
 (0)