Claude Code 最终输出归档:剔除 Thinking 与工具过程,打造自动化个人 Markdown 知识库(进阶篇)
这篇博客是上一篇Claude Code 提示词无感自动备份:Windows 与 Ubuntu 双平台配置指南进阶篇。
在上一篇文章中,我们实现了通过 Claude Code 的 Hook 机制自动将输入的 Prompt 记录到本地。但在实际使用中,仅存 Prompt 相当于“只有问题没有答案”。
为了把 Claude Code 真正变成随手可查的编码知识库,我们需要连同 Claude 的回复一并归档。
然而,Claude Code 在干活时会产生大量中间过程——比如漫长的思考(<thinking> 块)、频繁的工具调用(如 Write、Edit、Bash 执行结果)。如果直接把完整的 Context 存下来,日志会极其臃肿。
本文将介绍如何利用 Claude Code 的原生 Hook 与 Node.js 脚本,精确识别并提取 Claude 的“最终回答”,彻底剔除中间思考与工具执行过程,并自动按“项目/日期”生成 Markdown 知识库文件。
一、 核心判断逻辑:如何精准捕获“最终输出”?
Claude Code 在运行过程中会频繁触发各种 Hook 事件。要实现“只存最终回答”,核心在于解决以下 3 个判断问题:
1. Hook 事件精准判定
UserPromptSubmit:仅在用户刚按下回车发送提示词时触发,用于记录问题。Stop/SessionStop:在 Claude 完成一轮响应(或暂停)时触发。必须依赖此事件来捕获输出。- 注意:Claude Code 在触发
Stop事件时传给 stdin 的 JSON 中,依然会携带上一轮的prompt字段。如果仅靠“是否存在prompt字段”来判定,会导致Stop事件被误判为用户输入而直接跳过。脚本必须优先读取data.hook_event_name进行严格判定。
2. 过滤思考过程(Thinking Blocks)
在 Claude 开启 Extended Thinking 时,思考内容会被封装在 type: 'thinking' 的 Block 中,或者包含在 <thinking> 标签内。
- 判断方式:解析内容 Block 数组时,过滤掉所有
block.type === 'thinking'的节点,仅提取block.type === 'text'的内容。同时在字符串层级正则清理残余的<thinking>标签。
3. 过滤工具执行过程(Tool Use & Tool Results)
当 Claude 执行文件写入或终端命令时,会产生 tool_use 和 tool_result 节点。
- 判断方式:直接跳过所有 Tool 节点。当一轮 Assistant 的回复处理完毕后,只拼接最终渲染给用户的文本内容(例如:“已在 hello.html 生成好啦…”)。
4. 深度兜底(Transcript JSONL 倒序解析)
在部分 CLI 版本中,Stop 事件的 Payload 并不直接包含完整的消息历史,而是提供了一个指向本地转录日志的路径 transcript_path。脚本通过倒序读取 JSONL 文件,寻找最后一个 role === 'assistant' 且包含文本的节点,可以做到 100% 稳妥提取。
二、 核心 Node.js 脚本 (log_prompt.js)
这是一个零依赖(不需要 npm install 任何第三方包)的 Node.js 原生脚本。支持跨平台(Windows / Linux),自动识别当前工作区项目名并按日期生成 Markdown。
请将以下代码保存为 log_prompt.js:
const fs = require('fs');
const path = require('path');
const os = require('os');
let body = '';
process.stdin.on('data', chunk => { body += chunk; });
process.stdin.on('end', () => {
try {
if (!body.trim()) return;
const data = JSON.parse(body);
// 1. 获取工作目录与项目名称(提取最后一级目录名)
const cwd = data.cwd || process.cwd();
const fullProjectPath = path.resolve(cwd);
const projectName = path.basename(fullProjectPath) || 'global';
// 2. 格式化当前时间与日期
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`; // YYYY-MM-DD
const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false }); // HH:mm:ss
// 3. 构建归档目录:~/.claude/history/项目名/YYYY-MM-DD.md
const userHome = os.homedir() || process.env.USERPROFILE;
const baseDir = path.join(userHome, '.claude', 'history', projectName);
if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir, { recursive: true });
}
const mdFilePath = path.join(baseDir, `${dateStr}.md`);
// 如果是当天首条记录,先写入 Markdown 文档头
if (!fs.existsSync(mdFilePath)) {
const header = `# ${projectName} - 对话记录 (${dateStr})\n\n> **项目路径**:\`${fullProjectPath}\` \n> **创建时间**:${dateStr}\n\n---\n\n`;
fs.writeFileSync(mdFilePath, header, 'utf8');
}
// 4. 获取原生 Hook 事件名称
const eventName = data.hook_event_name || data.event || data.type || '';
// -------------------------------------------------------------
// 情况 A:用户提交提示词 (UserPromptSubmit)
// -------------------------------------------------------------
if (eventName === 'UserPromptSubmit') {
const prompt = data.prompt || data.content || (data.message && data.message.content);
if (prompt) {
const promptMarkdown = `## 💬 Prompt [${timeStr}]\n\n\`\`\`text\n${prompt}\n\`\`\`\n\n`;
fs.appendFileSync(mdFilePath, promptMarkdown, 'utf8');
}
return;
}
// -------------------------------------------------------------
// 情况 B:Claude 回复结束 (Stop / SessionStop)
// -------------------------------------------------------------
if (eventName === 'Stop' || eventName === 'SessionStop' || !eventName) {
let finalAnswer = '';
// 文本提取逻辑:只保留 type === 'text' 的节点
const extractTextFromContent = (content) => {
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.filter(block => block && block.type === 'text' && block.text)
.map(block => block.text)
.join('\n');
}
return '';
};
// 策略 1:直接从 stdin JSON 的 messages 解析
if (Array.isArray(data.messages) && data.messages.length > 0) {
const lastAssistantMsg = [...data.messages].reverse().find(m => m.role === 'assistant');
if (lastAssistantMsg) {
finalAnswer = extractTextFromContent(lastAssistantMsg.content);
}
}
// 策略 2:从 response / result / message 解析
if (!finalAnswer) {
if (data.last_assistant_message) {
finalAnswer = extractTextFromContent(data.last_assistant_message);
} else if (typeof data.response === 'string') {
finalAnswer = data.response;
} else if (typeof data.result === 'string') {
finalAnswer = data.result;
} else if (data.result && data.result.text) {
finalAnswer = data.result.text;
}
}
// 策略 3:从本地转录文件 (transcript_path JSONL) 倒序解析
if (!finalAnswer && data.transcript_path && fs.existsSync(data.transcript_path)) {
try {
const lines = fs.readFileSync(data.transcript_path, 'utf8').trim().split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
if (!lines[i].trim()) continue;
const entry = JSON.parse(lines[i]);
const msg = entry.message || entry;
if (msg && msg.role === 'assistant') {
const text = extractTextFromContent(msg.content);
if (text) {
finalAnswer = text;
break;
}
}
}
} catch (e) {
// ignore error
}
}
// 深度清洗:彻底清空 <thinking> 标签
if (finalAnswer) {
finalAnswer = finalAnswer.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
}
// 写入 Markdown
if (finalAnswer) {
const responseMarkdown = `### 🤖 Claude Output [${timeStr}]\n\n${finalAnswer}\n\n---\n\n`;
fs.appendFileSync(mdFilePath, responseMarkdown, 'utf8');
}
}
} catch (err) {
// 异常静默,避免阻塞终端运行
}
});
三、 双平台部署指南
在两个平台下,核心思想都是:将 log_prompt.js 放到 Hook 目录下,并在 settings.json 中配置对 UserPromptSubmit 与 Stop 的监听。
1. Windows 环境部署
第一步:放置脚本
在 C:\Users\<你的用户名>\.claude\hooks\ 目录下保存上述脚本为 log_prompt.js。
- 脚本完整路径:
C:\Users\<你的用户名>\.claude\hooks\log_prompt.js
第二步:配置 C:\Users\<你的用户名>\.claude\settings.json
(请将路径中的用户名替换为你真实的 Windows 用户名,例如 subk)
{
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"includeCoAuthoredBy": false,
"model": "opus",
"statusLine": {
"type": "command",
"command": "node C:/Users/subk/.claude/hooks/statusline.js"
},
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node C:/Users/subk/.claude/hooks/log_prompt.js"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node C:/Users/subk/.claude/hooks/log_prompt.js"
}
]
}
]
}
}
2. Ubuntu / Linux 环境部署
第一步:放置脚本
在 ~/.claude/hooks/ 目录下保存上述脚本为 log_prompt.js。
mkdir -p ~/.claude/hooks
# 将脚本写入 ~/.claude/hooks/log_prompt.js
第二步:配置 ~/.claude/settings.json
{
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"includeCoAuthoredBy": false,
"model": "opus",
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
},
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/hooks/log_prompt.js"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/hooks/log_prompt.js"
}
]
}
]
}
}
四、 成果展示与效果
配置完成后,打开 Terminal 正常使用 claude 即可。归档日志会自动保存在:
~/.claude/history/<项目名称>/YYYY-MM-DD.md
目录结构预览:
~/.claude/history/
├── my-web-app/
│ ├── 2026-08-06.md
│ └── 2026-08-07.md
└── temp-script/
└── 2026-08-07.md
生成的 Markdown 文件样例 (2026-08-07.md):
my-web-app – 对话记录 (2026-08-07)
项目路径:
/home/subk/projects/my-web-app创建时间:2026-08-07
💬 Prompt
帮我写个10行左右的HTML代码,标题是你好。其余随意填充。🤖 Claude Output
已在 hello.html 生成好啦,标题是”你好”,正文简单填了两句欢迎语,一共10行。
可以看到,中间所有文件写入的过程(如 Write(hello.html) 过程)、命令渲染以及后台思考全被干净地剔除了,只留下了干净对齐的 Prompt 与最终结论。直接用 VS Code 或 Obsidian 打开就是一套排版精美的知识积累文档!