fix: bypass GateGuard file gates in subagents (#1710)

This commit is contained in:
Affaan Mustafa
2026-05-11 01:51:24 -04:00
committed by GitHub
parent f8a0c4f884
commit 7b964402ee
11 changed files with 200 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Instructions
This is a **production-ready AI coding plugin** providing 48 specialized agents, 182 skills, 68 commands, and automated hook workflows for software development.
This is a **production-ready AI coding plugin** providing 48 specialized agents, 185 skills, 68 commands, and automated hook workflows for software development.
**Version:** 2.0.0-rc.1
@@ -146,7 +146,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
```
agents/ — 48 specialized subagents
skills/ — 182 workflow skills and domain knowledge
skills/ — 185 workflow skills and domain knowledge
commands/ — 68 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)

View File

@@ -350,7 +350,7 @@ If you stacked methods, clean up in this order:
/plugin list everything-claude-code@everything-claude-code
```
**That's it!** You now have access to 48 agents, 182 skills, and 68 legacy command shims.
**That's it!** You now have access to 48 agents, 185 skills, and 68 legacy command shims.
### Dashboard GUI
@@ -1338,7 +1338,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|---------|-------------|----------|--------|
| Agents | PASS: 48 agents | PASS: 12 agents | **Claude Code leads** |
| Commands | PASS: 68 commands | PASS: 31 commands | **Claude Code leads** |
| Skills | PASS: 182 skills | PASS: 37 skills | **Claude Code leads** |
| Skills | PASS: 185 skills | PASS: 37 skills | **Claude Code leads** |
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
@@ -1443,7 +1443,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|---------|------------|------------|-----------|----------|
| **Agents** | 48 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
| **Commands** | 68 | Shared | Instruction-based | 31 |
| **Skills** | 182 | Shared | 10 (native format) | 37 |
| **Skills** | 185 | Shared | 10 (native format) | 37 |
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |

View File

@@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list everything-claude-code@everything-claude-code
```
**完成!** 你现在可以使用 48 个代理、182 个技能和 68 个命令。
**完成!** 你现在可以使用 48 个代理、185 个技能和 68 个命令。
### multi-* 命令需要额外配置

View File

@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — 智能体指令
这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、182 项技能、68 条命令以及自动化钩子工作流,用于软件开发。
这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、185 项技能、68 条命令以及自动化钩子工作流,用于软件开发。
**版本:** 2.0.0-rc.1
@@ -147,7 +147,7 @@
```
agents/ — 48 个专业子代理
skills/ — 182 个工作流技能和领域知识
skills/ — 185 个工作流技能和领域知识
commands/ — 68 个斜杠命令
hooks/ — 基于触发的自动化
rules/ — 始终遵循的指导方针(通用 + 每种语言)

View File

@@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list everything-claude-code@everything-claude-code
```
**搞定!** 你现在可以使用 48 个智能体、182 项技能和 68 个命令了。
**搞定!** 你现在可以使用 48 个智能体、185 项技能和 68 个命令了。
***
@@ -1134,7 +1134,7 @@ opencode
|---------|-------------|----------|--------|
| 智能体 | PASS: 48 个 | PASS: 12 个 | **Claude Code 领先** |
| 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** |
| 技能 | PASS: 182 项 | PASS: 37 项 | **Claude Code 领先** |
| 技能 | PASS: 185 项 | PASS: 37 项 | **Claude Code 领先** |
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
@@ -1242,7 +1242,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|---------|------------|------------|-----------|----------|
| **智能体** | 48 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
| **命令** | 68 | 共享 | 基于指令 | 31 |
| **技能** | 182 | 共享 | 10 (原生格式) | 37 |
| **技能** | 185 | 共享 | 10 (原生格式) | 37 |
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |

View File

@@ -376,6 +376,21 @@ function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID]) {
].join('\n');
}
function isSubagentInvocation(data) {
if (!data || typeof data !== 'object') {
return false;
}
const candidates = [
data.agent_id,
data.agentId,
data.parent_tool_use_id,
data.parentToolUseId
];
return candidates.some(candidate => typeof candidate === 'string' && candidate.trim());
}
// --- Deny helper ---
function denyResult(reason, options = {}) {
@@ -422,6 +437,7 @@ function run(rawInput) {
// Normalize: case-insensitive matching via lookup map
const TOOL_MAP = { edit: 'Edit', write: 'Write', multiedit: 'MultiEdit', bash: 'Bash' };
const toolName = TOOL_MAP[rawToolName.toLowerCase()] || rawToolName;
const inSubagent = isSubagentInvocation(data);
if (toolName === 'Edit' || toolName === 'Write') {
const filePath = toolInput.file_path || '';
@@ -429,6 +445,10 @@ function run(rawInput) {
return rawInput; // allow
}
if (inSubagent) {
return rawInput; // parent session already passed the first-touch file gate
}
if (!isChecked(filePath)) {
if (!markChecked(filePath)) {
return allowWithStateWarning();
@@ -440,6 +460,10 @@ function run(rawInput) {
}
if (toolName === 'MultiEdit') {
if (inSubagent) {
return rawInput; // parent session already passed the first-touch file gate
}
const edits = toolInput.edits || [];
for (const edit of edits) {
const filePath = edit.file_path || '';

View File

@@ -32,7 +32,6 @@ Flox environments are defined in `.flox/env/manifest.toml` and activated with `f
- `$FLOX_ENV_CACHE` — Persistent local storage for caches, venvs, data (survives rebuilds)
- `$FLOX_ENV_PROJECT` — Project root directory (where `.flox/` lives)
## Essential Commands
```bash

View File

@@ -131,7 +131,7 @@ Options: `--size <pt>` (default: 68), `--color <hex>` (default: 8E8E93), `--weig
| `carbon` | Carbon | 2400+ | IBM design language |
| `heroicons` | HeroIcons | 1200+ | Tailwind CSS companion |
Browse all: https://icon-sets.iconify.design/
Browse all: <https://icon-sets.iconify.design/>
## Scripts Reference

View File

@@ -20,7 +20,7 @@ Architecture and implementation patterns for building modules with the **tinystr
## How It Works
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` component should be used for JSON serialization to maintain a zero-dependency footprint. The framework also includes a utility in `ApplicationManager` to bootstrap the project's execution environment by generating the `bin/dispatcher` script.

View File

@@ -6,7 +6,7 @@ Use the testing patterns described here when writing units tests for your tinyst
## How It Works
Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered.
Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered.
Because the `ActionRegistry` is a singleton, it is critical to maintain isolation between tests by properly initializing your application state before each test execution, preventing side effects from leaking across the test suite.

View File

@@ -981,6 +981,168 @@ function runTests() {
assert.ok(fs.existsSync(freshState), 'fresh state file should remain');
})) passed++; else failed++;
function runFreshSessionEdit(filePath, extra = {}) {
return runHook({
tool_name: 'Edit',
tool_input: { file_path: filePath, old_string: 'a', new_string: 'b' },
session_id: 'subagent-fresh-session',
...extra
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
}
function runFreshSessionBash(command, extra = {}) {
return runBashHook({
tool_name: 'Bash',
tool_input: { command },
session_id: 'subagent-fresh-session',
...extra
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
}
// --- Test 30: top-level Edit denies; subagent Edit allows ---
clearState();
if (test('A/B: same Edit denies at top level and allows with agent_id', () => {
const topLevel = runFreshSessionEdit('/src/subagent-edit.js');
const topOut = parseOutput(topLevel.stdout);
assert.ok(topOut, 'top-level edit should produce JSON output');
assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny');
clearState();
const subagent = runFreshSessionEdit('/src/subagent-edit.js', { agent_id: 'agent-abc-123' });
const subOut = parseOutput(subagent.stdout);
assert.ok(subOut, 'subagent edit should produce JSON output');
assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny',
'subagent edit should bypass the first-touch file gate');
})) passed++; else failed++;
// --- Test 31: top-level Write denies; subagent Write allows ---
clearState();
if (test('A/B: same Write denies at top level and allows with agent_id', () => {
const topLevel = runHook({
tool_name: 'Write',
tool_input: { file_path: '/src/subagent-write.js', content: 'module.exports = {};' },
session_id: 'subagent-fresh-session'
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
const topOut = parseOutput(topLevel.stdout);
assert.ok(topOut, 'top-level write should produce JSON output');
assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny');
clearState();
const subagent = runHook({
tool_name: 'Write',
tool_input: { file_path: '/src/subagent-write.js', content: 'module.exports = {};' },
session_id: 'subagent-fresh-session',
agent_id: 'agent-abc-123'
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
const subOut = parseOutput(subagent.stdout);
assert.ok(subOut, 'subagent write should produce JSON output');
assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny',
'subagent write should bypass the first-touch file gate');
})) passed++; else failed++;
// --- Test 32: top-level MultiEdit denies; subagent MultiEdit allows ---
clearState();
if (test('A/B: same MultiEdit denies at top level and allows with agent_id', () => {
const edits = [
{ file_path: '/src/subagent-multi-a.js', old_string: 'a', new_string: 'b' },
{ file_path: '/src/subagent-multi-b.js', old_string: 'c', new_string: 'd' }
];
const topLevel = runHook({
tool_name: 'MultiEdit',
tool_input: { edits },
session_id: 'subagent-fresh-session'
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
const topOut = parseOutput(topLevel.stdout);
assert.ok(topOut, 'top-level MultiEdit should produce JSON output');
assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny');
clearState();
const subagent = runHook({
tool_name: 'MultiEdit',
tool_input: { edits },
session_id: 'subagent-fresh-session',
agent_id: 'agent-abc-123'
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
const subOut = parseOutput(subagent.stdout);
assert.ok(subOut, 'subagent MultiEdit should produce JSON output');
assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny',
'subagent MultiEdit should bypass the first-touch file gate');
})) passed++; else failed++;
// --- Test 33: Bash stays gated inside subagents ---
clearState();
if (test('routine Bash remains gated in subagent context', () => {
const result = runFreshSessionBash('pwd', { agent_id: 'agent-abc-123' });
const output = parseOutput(result.stdout);
assert.ok(output, 'subagent Bash should produce JSON output');
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request'));
})) passed++; else failed++;
// --- Test 34: destructive Bash stays gated inside subagents ---
clearState();
if (test('destructive Bash remains gated in subagent context', () => {
const result = runFreshSessionBash('rm -rf /tmp/demo-path', { agent_id: 'agent-abc-123' });
const output = parseOutput(result.stdout);
assert.ok(output, 'subagent destructive Bash should produce JSON output');
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive command detected'));
})) passed++; else failed++;
// --- Test 35: parent tool IDs also mark subagent context ---
clearState();
if (test('parent_tool_use_id and parentToolUseId mark subagent file edits', () => {
const snake = runFreshSessionEdit('/src/subagent-parent-snake.js', { parent_tool_use_id: 'toolu_parent_01' });
const snakeOut = parseOutput(snake.stdout);
assert.ok(snakeOut, 'snake-case parent marker should produce JSON output');
assert.ok(!snakeOut.hookSpecificOutput || snakeOut.hookSpecificOutput.permissionDecision !== 'deny',
'parent_tool_use_id should bypass the first-touch file gate');
clearState();
const camel = runFreshSessionEdit('/src/subagent-parent-camel.js', { parentToolUseId: 'toolu_parent_02' });
const camelOut = parseOutput(camel.stdout);
assert.ok(camelOut, 'camel-case parent marker should produce JSON output');
assert.ok(!camelOut.hookSpecificOutput || camelOut.hookSpecificOutput.permissionDecision !== 'deny',
'parentToolUseId should bypass the first-touch file gate');
})) passed++; else failed++;
// --- Test 36: only non-empty string markers count ---
clearState();
if (test('empty and non-string subagent markers do not bypass file gates', () => {
const cases = [
['empty', { agent_id: '' }],
['whitespace', { agent_id: ' ' }],
['numeric', { agent_id: 12345 }],
['null', { agent_id: null }]
];
for (const [name, extra] of cases) {
clearState();
const result = runFreshSessionEdit(`/src/subagent-marker-${name}.js`, extra);
const output = parseOutput(result.stdout);
assert.ok(output, `${name} marker should produce JSON output`);
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
`${name} marker should not bypass the first-touch file gate`);
}
})) passed++; else failed++;
// --- Test 37: two sequential subagent Edits on different files pass ---
clearState();
if (test('two sequential subagent Edits on different files both pass', () => {
const first = runFreshSessionEdit('/src/subagent-seq-a.js', { agent_id: 'agent-seq' });
const firstOut = parseOutput(first.stdout);
assert.ok(firstOut, 'first subagent edit should produce JSON output');
assert.ok(!firstOut.hookSpecificOutput || firstOut.hookSpecificOutput.permissionDecision !== 'deny',
'first subagent edit should pass');
const second = runFreshSessionEdit('/src/subagent-seq-b.js', { agent_id: 'agent-seq' });
const secondOut = parseOutput(second.stdout);
assert.ok(secondOut, 'second subagent edit should produce JSON output');
assert.ok(!secondOut.hookSpecificOutput || secondOut.hookSpecificOutput.permissionDecision !== 'deny',
'second subagent edit should pass even on a new file');
})) passed++; else failed++;
// Cleanup only the temp directory created by this test file.
try {
if (fs.existsSync(stateDir)) {