Skip to content

Commit 9f3c867

Browse files
committed
feat(trace): widen opencode tracing and add cursor hooks
The opencode plugin now traces bash tool invocations in addition to file-writing tools, and resolves every file touched by an apply_patch call instead of only a single filePath/path argument. Hook invocations are fire-and-forget so a missing or failing chainloop binary never blocks tool execution. Also adds .cursor/hooks.json so Cursor sessions emit session-start, session-end, and after-file-edit trace events. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev> Chainloop-Trace-Sessions: ses_0c830ab01ffeGQhnPohmihWjP5
1 parent 5bd05c9 commit 9f3c867

2 files changed

Lines changed: 87 additions & 22 deletions

File tree

‎.cursor/hooks.json‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"hooks": {
3+
"afterFileEdit": [
4+
{
5+
"command": "chainloop trace hook cursor after-file-edit",
6+
"timeout": 30
7+
}
8+
],
9+
"sessionEnd": [
10+
{
11+
"command": "chainloop trace hook cursor session-end",
12+
"timeout": 30
13+
}
14+
],
15+
"sessionStart": [
16+
{
17+
"command": "chainloop trace hook cursor session-start",
18+
"timeout": 30
19+
}
20+
]
21+
},
22+
"version": 1
23+
}
Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
import type { Plugin } from "@opencode-ai/plugin"
22

33
export const ChainloopTrace: Plugin = async ({ $ }) => {
4-
// The commit-msg hook links sessions to commits by cross-referencing
5-
// staged files against AI line attributions recorded by post-tool-use.
6-
// If no file-writing tools (edit, write, apply_patch) are invoked during
7-
// the session, there will be no attributions and the commit will not be
8-
// marked as AI-assisted.
94
const fileWritingTools = ["edit","write","apply_patch"]
5+
const commandTools = ["bash"]
106

11-
function filePathFromArgs(args: any): string {
12-
if (args?.filePath) return args.filePath
13-
if (args?.path) return args.path
14-
return ""
7+
function filePathsFromArgs(args: any): string[] {
8+
if (args?.filePath) return [args.filePath]
9+
if (args?.path) return [args.path]
10+
if (args?.patchText) return parsePatchPaths(args.patchText)
11+
return []
1512
}
1613

14+
// parsePatchPaths extracts affected file paths from an apply_patch
15+
// patchText payload. Each section starts with *** Add File:, *** Update
16+
// File:, or *** Delete File: followed by the path. Paths are deduplicated
17+
// while preserving first-seen order.
18+
function parsePatchPaths(patchText: string): string[] {
19+
const paths: string[] = []
20+
const seen = new Set<string>()
21+
const re = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm
22+
let m
23+
while ((m = re.exec(patchText)) !== null) {
24+
const p = m[1].trim()
25+
if (p && !seen.has(p)) {
26+
seen.add(p)
27+
paths.push(p)
28+
}
29+
}
30+
return paths
31+
}
32+
33+
// fire-and-forget: tracing must never block tool execution. If chainloop
34+
// is unavailable or errors, log to stderr and move on.
1735
async function fire(event: string, payload: Record<string, any>) {
1836
const json = JSON.stringify(payload)
19-
await $`echo ${json} | chainloop trace hook opencode ${event}`
37+
try {
38+
await $`echo ${json} | chainloop trace hook opencode ${event}`
39+
} catch (err) {
40+
console.error(`chainloop-trace: ${event} hook failed: ${err}`)
41+
}
2042
}
2143

2244
return {
@@ -31,22 +53,42 @@ export const ChainloopTrace: Plugin = async ({ $ }) => {
3153
}
3254
},
3355
"tool.execute.before": async (input, output) => {
56+
if (commandTools.includes(input.tool)) {
57+
await fire("pre-tool-use", {
58+
session_id: input.sessionID,
59+
hook_event_name: "tool.execute.before",
60+
tool_name: input.tool,
61+
})
62+
return
63+
}
3464
if (!fileWritingTools.includes(input.tool)) return
35-
await fire("pre-tool-use", {
36-
session_id: input.sessionID,
37-
hook_event_name: "tool.execute.before",
38-
tool_name: input.tool,
39-
file_path: filePathFromArgs(output.args),
40-
})
65+
for (const fp of filePathsFromArgs(output.args)) {
66+
await fire("pre-tool-use", {
67+
session_id: input.sessionID,
68+
hook_event_name: "tool.execute.before",
69+
tool_name: input.tool,
70+
file_path: fp,
71+
})
72+
}
4173
},
4274
"tool.execute.after": async (input) => {
75+
if (commandTools.includes(input.tool)) {
76+
await fire("post-tool-use", {
77+
session_id: input.sessionID,
78+
hook_event_name: "tool.execute.after",
79+
tool_name: input.tool,
80+
})
81+
return
82+
}
4383
if (!fileWritingTools.includes(input.tool)) return
44-
await fire("post-tool-use", {
45-
session_id: input.sessionID,
46-
hook_event_name: "tool.execute.after",
47-
tool_name: input.tool,
48-
file_path: filePathFromArgs(input.args),
49-
})
84+
for (const fp of filePathsFromArgs(input.args)) {
85+
await fire("post-tool-use", {
86+
session_id: input.sessionID,
87+
hook_event_name: "tool.execute.after",
88+
tool_name: input.tool,
89+
file_path: fp,
90+
})
91+
}
5092
},
5193
}
5294
}

0 commit comments

Comments
 (0)