Skip to content

fix: 自动注入 settings.local.json 解决 bot 场景 Bash 权限失败#257

Merged
lishuceo merged 3 commits into
mainfrom
feat/claude-session-4aaad3
Jun 8, 2026
Merged

fix: 自动注入 settings.local.json 解决 bot 场景 Bash 权限失败#257
lishuceo merged 3 commits into
mainfrom
feat/claude-session-4aaad3

Conversation

@lishuceo

@lishuceo lishuceo commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • workspace 初始化后自动写入 .claude/settings.local.json,预置 git/npm/npx/gh 等基础命令白名单
  • 解决 bot 无终端场景下 Bash 写命令报 "Tool permission request failed: Error: Stream closed" 的问题
  • 已存在的 settings.local.json 不会被覆盖,保留用户自定义配置

Root Cause

SDK 的 permissionMode: 'acceptEdits' 只自动批准 Edit/Write。对于不在项目 permissions.allow 中的 Bash 命令,SDK 会尝试交互式权限审批,在无终端的飞书 bot 环境下直接失败。

Test plan

  • 新建 workspace 后确认 .claude/settings.local.json 存在且包含正确白名单
  • 已有 settings.local.json 的 workspace 不被覆盖
  • 在受限项目(如 urhox-bench)中执行 git add/git commit 不再报 Stream closed

🤖 Generated with Claude Code

在 bot/飞书场景下,项目级 .claude/settings.json 的 permissions.allow
白名单过严会导致 Bash 写命令触发交互式权限审批,但无终端环境下
审批请求无法送达,报 "Stream closed" 错误。

现在 setupWorkspace 在 clone 完成后自动注入 .claude/settings.local.json,
预置 git/npm/npx/gh 等基础命令的白名单,覆盖项目级限制。
已存在的 settings.local.json 不会被覆盖。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lishuceo's task in 4m 1s —— View job


Code Review: 自动注入 settings.local.json

Verdict: ✅ Approved — clean, well-scoped fix. One test-quality suggestion + a couple of minor notes below.

  • Read CLAUDE.md & PR intent
  • Review src/workspace/manager.ts
  • Review src/__tests__/workspace-settings-injection.test.ts
  • Trace permission flow (executor.ts canUseTool, security.ts)
  • Post review

Assessment

The fix is sound. injectLocalSettings() (manager.ts:237) writes a basic command allowlist into the isolated workspace clone, skips if a file already exists, and swallows write errors non-fatally (logger.warn + continue) — all correct for a best-effort augmentation.

Worth noting for context: canUseTool in executor.ts:905-911 already auto-allows all Bash commands, so this injection does not widen the effective permission set — it only stops the SDK's settings-layer pre-check from prompting/blocking before canUseTool is reached. So no net security regression from the broad npm/npx/gh entries.

🟡 Test does not exercise the real function (confidence: 80)

src/__tests__/workspace-settings-injection.test.ts re-implements the settings object and write logic inline rather than importing the real code. Because injectLocalSettings is not exported, the test can't catch drift — if the allowlist in manager.ts changes (or the skip-if-exists guard breaks), these tests stay green. Per CLAUDE.md's testing policy this should cover the actual function.

Suggestion: export function injectLocalSettings(...), then in the test call it against a temp dir and assert (1) the file is created with the expected permissions.allow, and (2) a second call leaves a pre-existing file untouched. That also exercises the mkdirSync(claudeDir) branch the current test skips.

Fix this →

Minor notes (non-blocking)

  • Readonly workspaces also get the injectioninjectLocalSettings(workspacePath) is called at manager.ts:169, before the mode === 'writable' branch, so readonly analysis workspaces also receive a write-command allowlist. Harmless (the readOnly gate in canUseTool still blocks writes), but the allowlist is unnecessary there.
  • Untracked file in the clone — the injected .claude/settings.local.json shows up as untracked in the workspace. This is mitigated because the agent prompt explicitly forbids git add . / git add -A (executor.ts:448, pipeline/prompts.ts:111), so it shouldn't leak into PRs — but it relies on the model obeying that instruction for repos that don't already gitignore the file.
    · branch feat/claude-session-4aaad3

Comment thread src/__tests__/workspace-settings-injection.test.ts Outdated
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lishuceo's task in 2m 37s —— View job


Code Review (re-review on synchronize): 自动注入 settings.local.json

  • Read CLAUDE.md & PR intent
  • Review src/workspace/manager.ts (injectLocalSettings)
  • Review src/__tests__/workspace-settings-injection.test.ts
  • Check status of previously-flagged issue
  • Post review

Verdict: ⚠️ One previously-flagged issue still unaddressed — the production fix itself remains sound.

Production code — ✅ good

injectLocalSettings() (manager.ts:237) is correct: writes a basic command allowlist into the isolated workspace clone, skips when a file already exists (manager.ts:242), creates .claude/ on demand (manager.ts:267), and swallows write errors non-fatally (logger.warn + continue). No security regression — canUseTool in executor.ts already auto-allows Bash, so this only stops the SDK settings-layer pre-check from blocking before canUseTool is reached.

🟡 Test still re-implements logic instead of calling the real function (confidence: 85)

This was flagged in the prior review and is not yet addressed in the current revision:

  • manager.ts:237injectLocalSettings is still declared function injectLocalSettings (not exported).
  • src/__tests__/workspace-settings-injection.test.ts:38-56, 82-99 — the settings object and write logic are copy-pasted inline; the test imports nothing from manager.ts.

Consequence: the assertions pass against locally-constructed objects, so if the real allowlist in manager.ts drifts or the skip-if-exists guard regresses, the tests stay green. Per CLAUDE.md's testing policy, new functions should be covered against the actual implementation.

Suggested fix:

  1. export function injectLocalSettings(...) in manager.ts.
  2. In the test, import { injectLocalSettings } and call it against a temp dir, then assert: (a) file created with the expected permissions.allow, and (b) a second call leaves a pre-existing file untouched. This also exercises the mkdirSync(claudeDir) branch the current test pre-creates manually.

Fix this →

Minor notes (non-blocking, unchanged from prior review)

  • Readonly workspaces also get the injectioninjectLocalSettings() is called at manager.ts:169, before the mode === 'writable' branch, so readonly analysis workspaces also receive a write-command allowlist. Harmless (the readOnly gate in canUseTool still blocks writes), just unnecessary there.
  • Untracked file in the clone — the injected .claude/settings.local.json is untracked in the workspace; mitigated by the prompt forbidding git add . / git add -A, but relies on the model obeying that for repos that don't gitignore the file.

I can apply the export + test rewrite if you'd like — just say the word.
· branch feat/claude-session-4aaad3

Review 指出测试重新实现了逻辑而非调用真实函数,导致无法检测漂移。
现在导出 injectLocalSettings 并在测试中直接 import 调用,覆盖:
- 文件创建 + 白名单内容验证
- .claude 目录不存在时自动创建
- 已存在文件不被覆盖

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lishuceo's task in 2m 53s —— View job


Re-review (synchronize): 自动注入 settings.local.json

  • Read CLAUDE.md & PR intent
  • Check status of previously-flagged test issue (export + real-call rewrite)
  • Review src/workspace/manager.ts
  • Review src/__tests__/workspace-settings-injection.test.ts
  • Post review

Verdict: ✅ Approved — 1 previous issue fixed. No new issues.

Previously-flagged issue — ✅ Resolved

The "test re-implements logic instead of calling the real function" finding from the prior two reviews is now properly addressed:

  • manager.ts:237injectLocalSettings is now export function injectLocalSettings(...).
  • workspace-settings-injection.test.ts:6 — the test now import { injectLocalSettings } from '../workspace/manager.js' and calls the real implementation against temp dirs (correct .js ESM extension per CLAUDE.md).

Coverage is solid and exercises the actual code path:

  • :25 file creation + allowlist contents (Bash(git *), npm, npx, gh)
  • :38 auto-creation of .claude/ via the real mkdirSync branch (asserts the dir doesn't exist beforehand)
  • :48 skip-if-exists — pre-writes a custom file and asserts it's left untouched
  • :62 critical bot-workflow command set

This now satisfies CLAUDE.md's testing policy (新功能必须附带单元测试 against the real implementation).

Production code — ✅ good (unchanged from prior reviews)

injectLocalSettings() writes a basic command allowlist into the isolated workspace clone, skips when a file already exists (:242), creates .claude/ on demand (:267), and swallows write errors non-fatally (logger.warn + continue, :272). All node:fs/node:path imports are present (manager.ts:2,4). No security regression — canUseTool in executor.ts already auto-allows Bash, so this only stops the SDK settings-layer pre-check from blocking before canUseTool is reached.

Minor notes (non-blocking, carried over — author's discretion)

  • Readonly workspaces also get the injectioninjectLocalSettings() is called at manager.ts:169, before the mode === 'writable' branch, so readonly analysis workspaces also receive the allowlist. Harmless (the readOnly gate in canUseTool still blocks writes), just unnecessary there.
  • Untracked file in the clone — the injected .claude/settings.local.json is untracked; mitigated by the prompt forbidding git add . / git add -A, but relies on the model obeying that for repos that don't gitignore the file.

Note: I could not run npx vitest run in this environment (command not in the allowed list), so the green status above is from code inspection, not execution. The test logic is straightforward and correct.

@lishuceo lishuceo merged commit 7107288 into main Jun 8, 2026
6 checks passed
@lishuceo lishuceo deleted the feat/claude-session-4aaad3 branch June 8, 2026 08:30
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