Observe tool timing and outcomes without retaining arguments or output.

Part of OpenMod Lab, by Jerry Wang. MIT licensed.
Requires Claude Code 2.1.287+. Inspect the source before loading; native mods run with your user permissions.
claude plugin validate ./plugins/flight-recorder
claude --plugin-dir ./plugins/flight-recorder
Retains up to 300 metadata records in module memory. No tool arguments, paths, result text or exception messages are retained. /flight-recorder json returns the trace; /flight-recorder clear clears rows. Reload/session restart clears memory. Elapsed time includes permission waits and downstream hooks. Earlier handlers can hide calls.
hooks/register.js 76 lines1// OpenMod Flight Recorder · MIT
2// Retain metadata only. Never keep arguments or output.
3const LIMIT = 300;
4let rows = [];
5let sequence = 0;
6
7export function register(on) {
8 on('session.start', async ($, e, next) => {
9 await $.command.register({
10 name: 'flight-recorder',
11 description: 'Show tool timing metadata',
12 argumentHint: '[json|clear]'
13 });
14 return next(e);
15 });
16
17 on('tool.call', async ($, e, next) => {
18 const startedAt = await $.clock.now();
19 const row = {
20 id: ++sequence, tool: e.tool, startedAt,
21 durationMs: 0, status: 'running'
22 };
23 rows.push(row);
24 rows = rows.slice(-LIMIT);
25 $.ui.invalidate('ui.render');
26 try {
27 const result = await next(e);
28 row.status = result.deny !== undefined
29 ? 'denied' : result.isError ? 'error' : 'ok';
30 return result;
31 } catch (error) {
32 row.status = next.signal.aborted ? 'aborted' : 'error';
33 throw error;
34 } finally {
35 row.durationMs = Math.max(0,
36 (await $.clock.now()) - startedAt);
37 $.ui.invalidate('ui.render');
38 }
39 });
40
41 on('command.run', { command: 'flight-recorder' }, ($, e) => {
42 if (e.args.trim() === 'clear') {
43 rows = [];
44 $.ui.invalidate('ui.render');
45 return { text: 'Flight Recorder cleared.' };
46 }
47 if (e.args.trim() === 'json') {
48 return { text: JSON.stringify({
49 format: 'openmod-flight-recorder', version: 1,
50 privacy: 'tool names, timestamps and outcomes only',
51 events: rows
52 }, null, 2) };
53 }
54 return { text: rows.length
55 ? rows.map(r => r.id + '. ' + r.tool + ' · '
56 + r.status + ' · ' + r.durationMs + 'ms').join('\n')
57 : 'No tool calls yet.' };
58 });
59
60 on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
61 if (e.props.hasSurvey || !rows.length) return next(e);
62 const { Box, Text } = $.ui.resolve(e);
63 const last = rows[rows.length - 1];
64 const issues = rows.filter(r =>
65 r.status === 'error' || r.status === 'denied').length;
66 return Box({ paddingX: 1, children: [
67 Text({ color: issues ? 'yellow' : 'cyan',
68 children: 'Flight Recorder · ' + rows.length
69 + ' retained calls · ' + issues + ' issues' }),
70 Text({ dimColor: true,
71 children: 'Latest: ' + last.tool + ' ' + last.status
72 + ' · ' + last.durationMs + 'ms · /flight-recorder' })
73 ] });
74 });
75}
76