Skip to content

fix(core): make read_file respect git-ignore settings for consistency with list_directory - #6154

Closed
Alex-ai-future wants to merge 5 commits into
QwenLM:mainfrom
Alex-ai-future:fix/cant-qoute-file
Closed

fix(core): make read_file respect git-ignore settings for consistency with list_directory#6154
Alex-ai-future wants to merge 5 commits into
QwenLM:mainfrom
Alex-ai-future:fix/cant-qoute-file

Conversation

@Alex-ai-future

Copy link
Copy Markdown
Contributor

title: "fix(core): make read_file respect git-ignore settings for consistency with list_directory"

What this PR does

read_file previously only checked .qwenignore patterns and ignored .gitignore rules entirely, while list_directory respected both. This PR makes read_file respect the same git-ignore and qwen-ignore settings as list_directory, using the same configuration options and per-call override pattern.

Specifically:

  • Added file_filtering_options parameter to read_file (matching list_directory's API), allowing per-call override of respect_git_ignore and respect_qwen_ignore
  • Updated validation to read config.getFileFilteringOptions() as the default, then apply per-call overrides from the tool parameters
  • Uses the unified fileService.shouldIgnoreFile() API instead of the previous shouldQwenIgnoreFile()-only check
  • Distinct error messages for git-ignore vs qwen-ignore rejections

Why it's needed

The inconsistency between list_directory and read_file created a confusing state:

  • Agent could not discover git-excluded files via list_directory (filtered out)
  • Agent could not reference them via @path in TUI (silently skipped)
  • But if the agent obtained the path through other means (e.g., shell find), read_file would successfully read the file

This meant tools that work together in the same workflow behaved inconsistently — one blocked access to git-excluded files while another allowed it. Now both tools respect the same settings, and users can control the behavior globally via settings or per-call via file_filtering_options.

Reviewer Test Plan

How to verify

Run the read-file tests and confirm the new git-ignore tests pass:

cd packages/core && npx vitest run src/tools/read-file.test.ts

Verify the new tests under with .gitignore:

  • should throw error if path is ignored by .gitignore when respectGitIgnore is true — confirms git-ignore is enforced
  • should allow reading git-ignored files when respectGitIgnore is false — confirms per-call override works
  • should allow reading non-git-ignored files — confirms normal files are unaffected
  • should respect per-call file_filtering_options to override git-ignore — confirms parameter-level override of config settings

Evidence (Before & After)

Before: read_file validation only called shouldQwenIgnoreFile(), never checking git-ignore. Git-excluded files could be read if the path was known.

After: read_file calls shouldIgnoreFile() with both respectGitIgnore and respectQwenIgnore from config, with per-call override support via file_filtering_options.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

N/A — unit tests only.

Risk & Scope

  • Main risk or tradeoff: None — this is a focused validation change. No execution path behavior changed.
  • Not validated / out of scope: notebook-edit.ts has a similar qwen-ignore-only check — deferred to a follow-up.
  • Breaking changes / migration notes: None. Existing calls without file_filtering_options inherit the config default (same as before for qwen-ignore, now also respects git-ignore).

Linked Issues

Closes #6119

中文说明

这个 PR 做了什么

read_file 之前只检查 .qwenignore 规则,完全不遵守 .gitignore,而 list_directory 两者都遵守。这个 PR 让 read_filelist_directory 使用相同的 git-ignore 和 qwen-ignore 设置。

具体修改:

  • read_file 添加了 file_filtering_options 参数(与 list_directory API 一致),支持每次调用时覆盖 respect_git_ignorerespect_qwen_ignore
  • 校验逻辑改为读取 config.getFileFilteringOptions() 作为默认值,然后用工具参数覆盖
  • 使用统一的 fileService.shouldIgnoreFile() API,替代之前只检查 shouldQwenIgnoreFile() 的逻辑
  • git-ignore 和 qwen-ignore 有不同的错误提示

为什么需要

两个工具行为不一致导致困惑的状态:

  • Agent 无法通过 list_directory 发现 git-excluded 文件(被过滤)
  • 无法通过 TUI 的 @path 引用(被静默跳过)
  • 但如果通过其他方式拿到路径(如 shell find),read_file 却能正常读取

现在两个工具遵守相同的设置,用户可以通过全局设置控制,也可以在每次调用时通过 file_filtering_options 覆盖。

验证方式

cd packages/core && npx vitest run src/tools/read-file.test.ts

69 个测试全部通过,包括 4 个新增的 git-ignore 测试。

关联 Issue

Closes #6119

Alex-ai-future and others added 3 commits July 2, 2026 11:04
… with list_directory

read_file previously only checked .qwenignore patterns and ignored
.gitignore rules entirely, while list_directory respected both. This
makes read_file respect the same git-ignore and qwen-ignore settings
as list_directory, using the same configuration options and per-call
override pattern.

Added file_filtering_options parameter to read_file (matching
list_directory's API), allowing per-call override of respect_git_ignore
and respect_qwen_ignore. Updated validation to read
config.getFileFilteringOptions() as the default, then apply per-call
overrides from the tool parameters. Uses the unified
fileService.shouldIgnoreFile() API instead of the previous
shouldQwenIgnoreFile()-only check.

Closes QwenLM#6119

Signed-off-by: Alex <alex.tech.lab@outlook.com>

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review findings. Downgraded from Approve to Comment: CI failing: Test (ubuntu-latest, Node 22.x).

The workflow fixes (--method GET additions) are correct bugfixes, and the file_filtering_options addition to read_file is a clean, well-tested consistency improvement with list_directory. Build and all 69 tests pass locally.

— qwen3.7-max via Qwen Code /review

});
});
});
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Missing test for respect_qwen_ignore per-call override

The new file_filtering_options parameter exposes both respect_git_ignore and respect_qwen_ignore, but only respect_git_ignore has per-call override tests. The respect_qwen_ignore: false override path is completely untested — if the ?? fallback had a bug (e.g., always resolving to the config value regardless of the per-call override), no test would catch it.

Consider adding a test in the existing qwen-ignore describe block:

it('should allow reading qwen-ignored files when respect_qwen_ignore is false via per-call override', () => {
  const ignoredFilePath = path.join(tempRootDir, 'cursor-secret.txt');
  const invocation = tool.build({
    file_path: ignoredFilePath,
    file_filtering_options: { respect_qwen_ignore: false },
  });
  expect(typeof invocation).not.toBe('string');
});

— qwen3.7-max via Qwen Code /review

await fsp.writeFile(
path.join(tempRootDir, '.gitignore'),
['secret.env', 'ignored-dir/'].join('\n'),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Missing test for directory-level gitignore patterns

The .gitignore setup includes ignored-dir/ but no test reads a file from inside that directory. Directory-level gitignore patterns can behave differently from file-level patterns (the ignore library checks the full path including parent directory components). A bug where directory-pattern matches fail while file-pattern matches work would go undetected.

Consider adding:

it('should throw error for files inside a git-ignored directory', () => {
  const ignoredDirPath = path.join(tempRootDir, 'ignored-dir');
  const ignoredFilePath = path.join(ignoredDirPath, 'data.txt');
  expect(() => tool.build({ file_path: ignoredFilePath }))
    .toThrow(/\.gitignore/);
});

— qwen3.7-max via Qwen Code /review

DragonnZhang
DragonnZhang previously approved these changes Jul 2, 2026

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds file_filtering_options (with respect_git_ignore and respect_qwen_ignore) to the read_file tool, bringing it in line with list_directory which already honors these settings. The implementation correctly delegates to the existing shouldIgnoreFile / shouldGitIgnoreFile methods on the file service, with per-call overrides falling back to config defaults via nullish coalescing. Tests cover the key paths: git-ignored files rejected, non-ignored files allowed, and per-call override toggling. Looks correct.

— qwen3-coder via Qwen Code /review

@wenshao

wenshao commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the careful writeup and the repro in #6119 — the asymmetry you spotted is real. But I think this PR resolves it in the wrong direction, and I'd hold off on merging as-is.

The core problem: .gitignore is not an access-control list

With getFileFilteringOptions() defaulting to respectGitIgnore: true (DEFAULT_FILE_FILTERING_OPTIONS, and config.ts ?? true), this PR makes read_file refuse git-ignored files by default. The model won't pass respect_git_ignore: false on its own, so in practice every git-ignored path now hard-errors with File path '...' is ignored by .gitignore pattern(s).

.gitignore means "don't commit this to VCS" — not "don't let the agent read this." Files that are routinely git-ignored but that the agent legitimately needs to read:

  • .env / local config — to debug configuration and environment issues
  • dist/, build/ — to inspect compiled output
  • node_modules/ — to read dependency source and understand APIs
  • logs, coverage, generated code

Blocking these by default degrades normal agent workflows.

This is the opposite of your own conclusion in #6119

In the issue you wrote:

我认为这两个工具都不应该受到 Git 规则的影响。因为我们的仓库里有很多私有文件,本身是被 Git 屏蔽的,但在使用 Agent 工作时会需要到这些文件。如果按照上述规则,就没办法正常工作。

That reasoning ("neither tool should be affected by git rules, because we have private-but-needed git-ignored files") argues against this PR, not for it.

The discovery/read asymmetry is intentional, not a bug

list_directory / glob filter git-ignored entries for noise reduction — you don't want 50k node_modules files flooding a listing or the context window. read_file(<explicit path>) is a deliberate, named request — the agent already knows exactly what it wants. "Filter from a listing" and "hard-error on an explicit path" are different concerns; forcing them to be symmetric is the flawed premise.

What Claude Code / Codex do

Both treat gitignore as a discovery/UX filter, never as a gate on reads:

  • Claude Code — the Read tool has no gitignore check at all; reads are gated only by explicit permission deny rules. Its Glob doesn't respect .gitignore by default (opt-in via CLAUDE_CODE_GLOB_NO_IGNORE=false), and gitignore only kicks in for the @-completion picker / search — pure discovery UX.
  • Codex — file access is governed by sandbox scope (read-only / workspace-write / danger-full-access) + approval policy, i.e. by path, not by gitignore; gitignore is used for its file-search/@ discovery, not to block reading a named path.

If the real concern is leaking secrets (.env)

That's legitimate, but the right layer is an explicit ignore/deny mechanism — .qwenignore already exists for exactly "don't let the agent touch this." .gitignore is a poor proxy for "secret": it's mostly build noise, and plenty of real secrets aren't git-ignored at all.

Suggested change

The file_filtering_options per-call param itself is fine — the issue is purely the default and the "read must mirror list" premise. Either:

  1. Default respect_git_ignore to false for read_file (an explicit read honors explicit intent), keeping the per-call override as an opt-in; or
  2. Put it behind a default-off setting — never default-on.

One more ergonomics note: reusing the global respectGitIgnore means a user can't get "quiet listings but readable files" — turning it off to allow reads also floods list_directory with node_modules noise.

中文

感谢细致的分析和 #6119 里的复现 —— 你发现的不一致是真实存在的。但我认为这个 PR 把它修反了方向,建议先不要按现状合入。

核心问题:.gitignore 不是访问控制列表。 getFileFilteringOptions() 默认 respectGitIgnore: true,所以这个 PR 让 read_file 默认拒绝读取 git-ignored 文件。模型自己不会传 respect_git_ignore: false,于是实际效果是:所有被 git-ignore 的路径现在都会硬报错。

.gitignore 的语义是"不要提交到版本库",不是"不要让 agent 读"。被 git-ignore 但 agent 经常需要读的文件:.env/本地配置、dist/build/node_modules/、日志、coverage、生成代码。默认挡掉这些会损害正常工作流。

这与你自己在 #6119 的结论相反。 你在 issue 里写道:"我认为这两个工具都不应该受到 Git 规则的影响……如果按照上述规则,就没办法正常工作。" 这个论证是反对本 PR 的。

discovery / read 的不对称是有意的,不是 bug。 list_directory/glob 过滤 git-ignored 是为了降噪(别让 node_modules 淹没列表和上下文);read_file(具体路径)指名要读的明确动作。"从列表过滤"和"对指定路径硬报错"是两回事,强行对齐才是错误前提。

Claude Code / Codex 的做法 —— 两家都把 gitignore 当作发现/UX 层的降噪,从不用来拦截读取:

  • Claude Code:Read 工具完全没有 gitignore 检查,读取只受显式 permission deny 规则约束;Glob 默认尊重 .gitignoreCLAUDE_CODE_GLOB_NO_IGNORE=false 才 opt-in);gitignore 只用于 @ 补全/搜索这类纯发现场景。
  • Codex:文件访问由 sandbox scope(read-only/workspace-write/danger-full-access)+ 审批策略按路径管控,而非 gitignore;gitignore 只用于文件搜索/@ 发现。

如果真正担心的是 .env 泄密:这个诉求合理,但正确的图层是显式的 ignore/deny 机制 —— .qwenignore 就是干这个的。.gitignore 是很差的"密钥"代理:里面大多是构建噪音,而很多真正的密钥根本不在 gitignore 里。

建议改法file_filtering_options 这个 per-call 参数本身没问题,问题在默认值和"read 必须镜像 list"这个前提。二选一:

  1. read_filerespect_git_ignore 默认 false(指名读取尊重显式意图),per-call 覆盖作为 opt-in 保留;或
  2. 藏在一个默认关闭的设置后面,绝不 default-on。

另外一个 ergonomics 问题:复用全局 respectGitIgnore 意味着用户无法做到"列表降噪但文件可读"—— 关掉它以允许读取,会同时让 list_directorynode_modules 噪音淹没。

…eedback

read_file should not block git-excluded files by default. .gitignore
means 'do not commit', not 'do not read'. Default respect_git_ignore to
false, keeping per-call override as opt-in via file_filtering_options.
respect_qwen_ignore still follows config default.

Addresses review feedback on PR QwenLM#6154.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
if (fileService.shouldQwenIgnoreFile(params.file_path)) {
const configOpts = this.config.getFileFilteringOptions();
const respectGitIgnore =
params.file_filtering_options?.respect_git_ignore ?? false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] respectGitIgnore falls back to hardcoded false, ignoring the user's config setting and the global default.

Every other tool in the codebase uses a 3-level fallback chain: per-call ?? config ?? DEFAULT_FILE_FILTERING_OPTIONS. For example, ls.ts:

respectGitIgnore:
  this.params.file_filtering_options?.respect_git_ignore ??
  this.config.getFileFilteringOptions().respectGitIgnore ??
  DEFAULT_FILE_FILTERING_OPTIONS.respectGitIgnore,  // = true

Here, configOpts is fetched on the line above but configOpts.respectGitIgnore is never read — it's dead code. Since DEFAULT_FILE_FILTERING_OPTIONS.respectGitIgnore is true, this means list_directory blocks gitignored files by default but read_file silently reads them. This also contradicts the schema description at line 445 which says "Defaults to the value from settings."

The test at read-file.test.ts:1250 ("should allow reading git-ignored files by default") encodes this bug: the mock config has respectGitIgnore: true yet the test asserts the file is readable, which only passes because the config is never consulted.

Suggested change
params.file_filtering_options?.respect_git_ignore ?? false;
const respectGitIgnore =
params.file_filtering_options?.respect_git_ignore ??
configOpts.respectGitIgnore ??
DEFAULT_FILE_FILTERING_OPTIONS.respectGitIgnore;

Also add import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js'; at the top of the file (matching ls.ts and glob.ts).

The test "should allow reading git-ignored files by default" must also be updated — with the fix, the default config (respectGitIgnore: true) should block gitignored files:

it('should block git-ignored files by default when config respects git-ignore', () => {
  const ignoredFilePath = path.join(tempRootDir, 'secret.env');
  expect(() => tool.build({ file_path: ignoredFilePath })).toThrow(/\.gitignore/);
});

— qwen3.7-max via Qwen Code /review

const respectGitIgnore =
params.file_filtering_options?.respect_git_ignore ?? false;
const respectQwenIgnore =
params.file_filtering_options?.respect_qwen_ignore ??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] respectQwenIgnore uses a 2-level fallback (per-call ?? config) and omits the third-level DEFAULT_FILE_FILTERING_OPTIONS.respectQwenIgnore safety net that ls.ts includes. While getFileFilteringOptions() currently always returns a complete object, if it ever returned a partial (e.g., from a new config backend), respectQwenIgnore would silently become undefined (falsy), disabling qwen-ignore filtering with no warning.

Suggested change
params.file_filtering_options?.respect_qwen_ignore ??
const respectQwenIgnore =
params.file_filtering_options?.respect_qwen_ignore ??
configOpts.respectQwenIgnore ??
DEFAULT_FILE_FILTERING_OPTIONS.respectQwenIgnore;

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/tools/read-file.ts Outdated
fileService.shouldGitIgnoreFile(params.file_path)
) {
return `File path '${filePath}' is ignored by .gitignore pattern(s).`;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The error-message routing calls shouldGitIgnoreFile a second time after shouldIgnoreFile already matched internally. This double-check pattern duplicates work and can misattribute the cause: if a file is both git-ignored and qwen-ignored, the user only sees the .gitignore error and never learns about the .qwenignore match. More importantly, the two non-atomic calls could diverge if either method is refactored.

Consider replacing shouldIgnoreFile + re-check with sequential individual checks:

if (respectGitIgnore && fileService.shouldGitIgnoreFile(params.file_path)) {
  return `File path '${filePath}' is ignored by .gitignore pattern(s).`;
}
if (respectQwenIgnore && fileService.shouldQwenIgnoreFile(params.file_path)) {
  return `File path '${filePath}' is ignored by ${fileService.getQwenIgnoreFileDisplayForPath(params.file_path)} pattern(s).`;
}

— qwen3.7-max via Qwen Code /review

…dling

- Add comment explaining why respect_git_ignore defaults to false
  (.gitignore is for version control, not access control)
- Fix respect_qwen_ignore to use 3-level fallback chain:
  per-call ?? config ?? DEFAULT_FILE_FILTERING_OPTIONS
- Replace shouldIgnoreFile + re-check with sequential individual
  checks to avoid redundant double-calls and provide accurate
  error messages
- Fix schema description to match actual default (false, not
  'from settings')

Addresses review feedback on PR QwenLM#6154.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
file_filtering_options?: {
/**
* Whether to respect .gitignore patterns when reading files.
* Only available in git repositories. Defaults to settings value.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] JSDoc for respect_git_ignore says "Defaults to settings value." but the implementation at line 525 hardcodes ?? false, never consulting the settings value. The JSON schema description (line 446) correctly states "Defaults to false because .gitignore is for version control, not access control."

The TypeScript interface JSDoc and the JSON schema give opposite defaults. A future engineer adding file_filtering_options to another tool (e.g., notebook-edit) will read the interface as the contract and implement the wrong default.

Suggested change
* Only available in git repositories. Defaults to settings value.
* Whether to respect .gitignore patterns when reading files.
* Only available in git repositories. Defaults to false (not the settings value).
* .gitignore means "do not commit", not "do not read".

— qwen3.7-max via Qwen Code /review

},
respect_qwen_ignore: {
description:
'Optional: Whether to respect .qwenignore and configured custom Qwen ignore file patterns when reading files. Defaults to the value from settings.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] .qwenignore was previously unconditionally enforced by read_file (old code: if (fileService.shouldQwenIgnoreFile(...))). This PR makes it configurable per-call, allowing the LLM to pass respect_qwen_ignore: false and bypass .qwenignore protections.

While this matches the ls.ts pattern, read_file is the tool most likely to be targeted for reading sensitive files. Consider whether the per-call override should only be allowed to tighten protection (force true), never relax it. Alternatively, document this as an accepted tradeoff for API consistency.

— qwen3.7-max via Qwen Code /review

expect(typeof invocation).not.toBe('string');
});

it('should allow reading qwen-ignored files when respect_qwen_ignore is false via per-call override', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] All mock configs in this test file return respectQwenIgnore: true from getFileFilteringOptions() (lines 59, 1101, 1186). Since DEFAULT_FILE_FILTERING_OPTIONS.respectQwenIgnore is also true, the middle branch of the 3-level fallback (configOpts.respectQwenIgnore) can never be distinguished from the default. A regression that silently skips the config lookup would go undetected.

Consider adding a test with a mock config returning respectQwenIgnore: false, asserting that a qwen-ignored file is readable without per-call override. This pins the config-level fallback.

— qwen3.7-max via Qwen Code /review

@wenshao wenshao closed this Jul 3, 2026
@wenshao

wenshao commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

这个 PR 的方向不对。为避免自动化程序错误合入,先关闭。

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.

Bug: list_directory and read_file have inconsistent git-ignore handling

5 participants