Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
df07849
feat: surface MCP availability hints and gate MCP tools via config (#…
ksamaschke Nov 17, 2025
abef2d4
fix: allow docs/documentation writes in main scope allowlist (#247)
ksamaschke Nov 18, 2025
9be36e6
chore: add config presets and docs allowlist coverage (#248)
ksamaschke Nov 18, 2025
bcea744
docs: streamline README and docs index (#249)
ksamaschke Nov 18, 2025
cb86910
fix: allow docs heredoc writes without infra blocking (#250)
ksamaschke Nov 18, 2025
0ca9756
feat: inject best practices & memory guidance; keep exec pattern in a…
ksamaschke Nov 18, 2025
a921e85
fix: allow markdown when any path segment is docs (#259)
ksamaschke Nov 18, 2025
d5d2a01
fix: enforce parent-path before markdown segment allowlist (#261)
ksamaschke Nov 19, 2025
e4aa0a2
fix: markdown segment allowlist respects parent-path gate (#262)
ksamaschke Nov 19, 2025
a29073b
fix: enforce infra policy on full command including ssh wrapper (#264)
ksamaschke Nov 19, 2025
837ab56
fix: make doc fast-path allow literal markdown code (#269)
ksamaschke Nov 19, 2025
15b1705
feat: add reviewed workflow enforcement hook and tests
ksamaschke Nov 19, 2025
1f761c2
fix: workflow allows repeated actions per step
ksamaschke Nov 19, 2025
48c3567
chore: sync dev into dev-workflows (#283)
ksamaschke Nov 19, 2025
55b9b76
feat: enforce reviewed workflow sequence when enabled
ksamaschke Nov 19, 2025
fb38cc5
fix: match workflow steps against expected tool
ksamaschke Nov 19, 2025
263e8dd
fix: respect agent privileges in project-scope enforcement
ksamaschke Nov 19, 2025
b6ff43b
fix: keep install protection when main scope is agent (#285)
ksamaschke Nov 19, 2025
b6186ad
infra: harden doc fast-path and register workflow hook
ksamaschke Nov 19, 2025
f14ccd5
config: relax project boundary in main-scope-dev preset
ksamaschke Nov 20, 2025
6f1a657
scope: allow parent docs for main-scope-dev and fix marker tests
ksamaschke Nov 20, 2025
2c3691f
pm-constraints: honor env allow_parent_allowlist_paths in markdown fa…
ksamaschke Nov 20, 2025
1ad4b02
Merge pull request #287 from intelligentcode-ai/feature/workflow-doc-…
ksamaschke Nov 20, 2025
5d8dcf9
infra: allow <<- heredoc docs; resolve main merge conflicts
ksamaschke Nov 20, 2025
1e3b0e6
pm: let allowlisted docs bypass PM tool blacklist
ksamaschke Nov 20, 2025
a31ad1f
Merge pull request #290 from intelligentcode-ai/fix/pm-docs-bypass
ksamaschke Nov 20, 2025
ff352f9
merge feature/workflow-enforcement into dev-workflows (keep dev-workf…
ksamaschke Nov 21, 2025
a15ba0d
Revert "merge feature/workflow-enforcement into dev-workflows (keep d…
ksamaschke Nov 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [8.20.92] - 2025-11-19
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Version mismatch in CHANGELOG: Entry shows 8.20.92 but the PR title and root VERSION file indicate this is release 8.20.91. The 8.20.92 entry should either be renamed to 8.20.91 or moved to a separate PR if these are intended to be different releases.

Copilot uses AI. Check for mistakes.

### Changed
- Documentation fast-path now requires quoted heredoc delimiters or a substitution-free body; unquoted heredocs with command substitution fall back to full infra checks.
- Marker detection accepts hookInput or path and hashes the active working directory, keeping agent/PM context consistent when agents run from subdirectories.
- Workflow enforcement hook is now registered by default in installer templates (Ansible/PowerShell) raising production hooks to 16.
- Summary ALL-CAPS guard skips paths containing shell variables to avoid false positives on `$SUMMARY_FILE` style redirects.
- `icc.config.main-scope-dev.json` relaxes project boundary (`allow_parent_allowlist_paths: true`) so Main Scope/agents can work in sibling dirs while install protection still blocks `~/.claude`.

### Testing
- `bash tests/run-tests.sh`

## [8.20.91] - 2025-11-19

### Added
- Optional workflow enforcement hook (`workflow-enforcement.js`) that ensures the configured Task → Plan → Review → Execute → Review → Document sequence runs in order for both Main Scope and agents.
- Config support: `workflow.enforcement` block in `icc.config.default.json` plus a ready-to-use `icc.config.workflow-reviewed.json` preset.
- Integration tests for the workflow state machine (uses per-test config/state directories).

### Testing
- `bash tests/run-tests.sh`

## [8.20.90] - 2025-11-19

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ Then work conversationally:
- `config.main-scope.json` – coordination-only main scope (agents execute work)
- `config.strict-main-scope.json` – read-only/Task-only main scope (ultra-safe mode)
- `config.main-scope-dev.json` – Linux/macOS friendly preset where Main Scope may run curated `git`/`gh` commands locally while all guardrails (file naming, folders, git privacy, @codex review, best practices, memory output) remain enabled
- `config.workflow-reviewed.json` – Enables workflow enforcement (Task → Plan → Review → Execute → Review → Document) for Main Scope + agents

See `sample-configs/README.md` for usage instructions and run `make install CONFIG_FILE=sample-configs/<name>.json` to apply one system-wide.

- Toggle `enforcement.main_scope_has_agent_privileges: true` if you want the Main Scope treated exactly like an agent (strict main-scope enforcement, PM-only limits, doc routing, etc. all short-circuit). Default is `false`; `icc.config.main-scope-dev.json` turns it on for systems impacted by the V8 issue.
- Enable `enforcement.workflow` to require the Task → Plan → Review → Execute → Review → Document sequence (see `icc.config.workflow-reviewed.json` for the default step mapping).

## Documentation
- Start: [docs/index.md](docs/index.md)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.20.90
8.20.91
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Version mismatch: root VERSION file shows 8.20.91 but src/VERSION shows 8.20.92. These should be synchronized. Based on the PR title mentioning v8.20.91, update src/VERSION to 8.20.91 to match.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

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

P2 Badge Align root and source version numbers

The release metadata is inconsistent: CHANGELOG.md and src/VERSION both advertise 8.20.92, but the root VERSION file remains at 8.20.91, so any tooling that reads VERSION (many packaging/CI scripts do) will report the older version while the code and changelog indicate the newer one. Please bump the root version to match so the release number is unambiguous.

Useful? React with 👍 / 👎.

7 changes: 4 additions & 3 deletions ansible/roles/intelligent-claude-code/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@
- { type: 'command', command: 'node {{ claude_install_path }}/hooks/agent-infrastructure-protection.js', timeout: 5000 }
- { type: 'command', command: 'node {{ claude_install_path }}/hooks/config-protection.js', timeout: 5000 }
- { type: 'command', command: 'node {{ claude_install_path }}/hooks/pre-agenttask-validation.js', timeout: 5000 }
- { type: 'command', command: 'node {{ claude_install_path }}/hooks/workflow-enforcement.js', timeout: 5000 }
- { type: 'command', command: 'node {{ claude_install_path }}/hooks/project-scope-enforcement.js', timeout: 5000 }
- { type: 'command', command: 'node {{ claude_install_path }}/hooks/summary-file-enforcement.js', timeout: 5000 }
SessionStart:
Expand Down Expand Up @@ -365,13 +366,13 @@

- name: Report hook registration
debug:
msg: "All 15 production hooks configured in settings.json - comprehensive enforcement active"
msg: "All 16 production hooks configured in settings.json - comprehensive enforcement active"

when: settings_json_exists.stat.exists

- name: Display settings creation notice
debug:
msg: "Settings file created with all 15 production hooks - comprehensive enforcement active"
msg: "Settings file created with all 16 production hooks - comprehensive enforcement active"
when: not settings_json_exists.stat.exists and ansible_verbosity >= 1

# badges.md file removed - scoring system simplified to clean progress reporting
Expand All @@ -389,5 +390,5 @@
- "✅ Installation complete!"
- "📍 Location: {{ claude_install_path }}"
- "🤖 Virtual Team: 14 core roles + unlimited specialists"
- "🔒 Behavioral Hooks: All 15 production hooks active (PreToolUse, UserPromptSubmit, SubagentStop, Stop)"
- "🔒 Behavioral Hooks: All 16 production hooks active (PreToolUse, UserPromptSubmit, SubagentStop, Stop)"
- "🚀 Use @Role communication to activate team members"
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"type": "command",
"command": "node {{ claude_install_path }}/hooks/pre-agenttask-validation.js",
"timeout": 5000
}, {
"type": "command",
"command": "node {{ claude_install_path }}/hooks/workflow-enforcement.js",
"timeout": 5000
}, {
"type": "command",
"command": "node {{ claude_install_path }}/hooks/project-scope-enforcement.js",
Expand Down Expand Up @@ -78,4 +82,4 @@
}]
}]
}
}
}
11 changes: 11 additions & 0 deletions icc.config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@
"violation_logging": true,
"auto_correction": true,
"main_scope_has_agent_privileges": false,
"workflow": {
"enabled": false,
"steps": [
{ "name": "Task", "tools": ["Task"] },
{ "name": "Plan", "tools": ["Plan"] },
{ "name": "Review Plan", "tools": ["Review"] },
{ "name": "Execute", "tools": ["Execute"] },
{ "name": "Review Execute", "tools": ["Review"] },
{ "name": "Document", "tools": ["Document", "Write", "Edit"] }
]
},
"strict_main_scope": true,
"strict_main_scope_message": "Main scope is limited to coordination work only. Create AgentTasks via Task tool for all technical operations.",
"pm_allowed_bash_commands": [
Expand Down
7 changes: 4 additions & 3 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function Register-ProductionHooks {
)

try {
Write-Host " Registering all 15 production hooks in settings.json..." -ForegroundColor Gray
Write-Host " Registering all 16 production hooks in settings.json..." -ForegroundColor Gray

# Load or create settings
$Settings = Get-SettingsJson -SettingsPath $SettingsPath
Expand All @@ -189,6 +189,7 @@ function Register-ProductionHooks {
[PSCustomObject]@{ type = "command"; command = "node `"$HooksPath\agent-marker.js`""; timeout = 5000 }
[PSCustomObject]@{ type = "command"; command = "node `"$HooksPath\config-protection.js`""; timeout = 5000 }
[PSCustomObject]@{ type = "command"; command = "node `"$HooksPath\pre-agenttask-validation.js`""; timeout = 5000 }
[PSCustomObject]@{ type = "command"; command = "node `"$HooksPath\workflow-enforcement.js`""; timeout = 5000 }
[PSCustomObject]@{ type = "command"; command = "node `"$HooksPath\project-scope-enforcement.js`""; timeout = 5000 }
[PSCustomObject]@{ type = "command"; command = "node `"$HooksPath\summary-file-enforcement.js`""; timeout = 5000 }
)
Expand Down Expand Up @@ -238,7 +239,7 @@ function Register-ProductionHooks {
$JsonOutput = $Settings | ConvertTo-Json -Depth 10
Set-Content -Path $SettingsPath -Value $JsonOutput -Encoding UTF8

Write-Host " ✅ All 15 production hooks registered successfully in settings.json" -ForegroundColor Green
Write-Host " ✅ All 16 production hooks registered successfully in settings.json" -ForegroundColor Green

} catch {
Write-Warning " Failed to register production hooks in settings.json: $($_.Exception.Message)"
Expand All @@ -254,7 +255,7 @@ function Install-HookSystem {
[string]$SourceDir
)

Write-Host "Installing hook system (15 production hooks)..." -ForegroundColor Yellow
Write-Host "Installing hook system (16 production hooks)..." -ForegroundColor Yellow

try {
# Create hooks directory structure
Expand Down
5 changes: 3 additions & 2 deletions sample-configs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ Configs included (all prefixed `icc.config.*`)
- `icc.config.relaxed.json` — Looser for local hacking: markdown outside allowlist permitted, parent paths allowed, blocking disabled.
- `icc.config.strict-main-scope.json` — Hard lock for Main Scope: no Write/Edit/Bash; delegation only; agents perform work under standard path restrictions.
- `icc.config.local-backup.json` — Snapshot of the previously active local config (before switching to the main-scope variant). Safety copy only.
- `icc.config.main-scope-dev.json` — Linux-friendly profile where Main Scope can run curated git/gh commands (e.g., merging PRs) without spawning agents; guardrails, privacy, and @codex review reminder remain enabled.
- `icc.config.main-scope-dev.json` — Linux-friendly profile where Main Scope can run curated git/gh commands (e.g., merging PRs) without spawning agents; guardrails, privacy, and @codex review reminder remain enabled. Project boundary is relaxed (`allow_parent_allowlist_paths: true`) so Main Scope/agents can work in sibling directories while still blocking `~/.claude`.
- `icc.config.workflow-reviewed.json` — Enables workflow enforcement (Task → Plan → Review → Execute → Review → Document) for both Main Scope and agents.

Main-scope agent privileges
- The new `enforcement.main_scope_has_agent_privileges` flag controls whether the Main Scope is treated like an agent (skips PM-only write limits, uses the agent allowlists). All presets keep it `false` except `icc.config.main-scope-dev.json`, which sets it to `true` so Ops/Dev work can run directly from the Main Scope.
- The `enforcement.main_scope_has_agent_privileges` flag controls whether the Main Scope is treated like an agent (skips PM-only write limits, uses the agent allowlists). All presets keep it `false` except `icc.config.main-scope-dev.json`, which sets it to `true` so Ops/Dev work can run directly from the Main Scope.

How the PR review reminder is activated
- Each config sets `enforcement.require_codex_review_comment: true`.
Expand Down
2 changes: 1 addition & 1 deletion sample-configs/icc.config.main-scope-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"enforcement": {
"blocking_enabled": true,
"strict_main_scope": false,
"allow_parent_allowlist_paths": false,
"allow_parent_allowlist_paths": true,
"allow_markdown_outside_allowlist": false,
"allow_markdown_outside_allowlist_agents": false,
"main_scope_has_agent_privileges": true,
Expand Down
17 changes: 17 additions & 0 deletions sample-configs/icc.config.workflow-reviewed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"enforcement": {
"blocking_enabled": true,
"main_scope_has_agent_privileges": false,
"workflow": {
"enabled": true,
"steps": [
{ "name": "Task", "tools": ["Task"] },
{ "name": "Plan", "tools": ["Plan"] },
{ "name": "Review Plan", "tools": ["Review"] },
{ "name": "Execute", "tools": ["Execute"] },
{ "name": "Review Execute", "tools": ["Review"] },
{ "name": "Document", "tools": ["Document", "Write", "Edit"] }
]
}
}
}
2 changes: 1 addition & 1 deletion src/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.20.90
8.20.92
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Version mismatch: src/VERSION shows 8.20.92 but root VERSION file shows 8.20.91. These should be synchronized. Based on the PR title mentioning v8.20.91, this should be 8.20.91 to match the root VERSION file.

Suggested change
8.20.92
8.20.91

Copilot uses AI. Check for mistakes.
27 changes: 23 additions & 4 deletions src/hooks/agent-infrastructure-protection.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,30 @@ function main() {
return false;
}

const heredocMatch = firstLine.match(/<<-?\s*'?([A-Za-z0-9_:-]+)'?/);
const dashTrim = firstLine.includes('<<-');
const heredocMatch = firstLine.match(/<<-?\s*(?:'([A-Za-z0-9_:-]+)'|"([A-Za-z0-9_:-]+)"|([A-Za-z0-9_:-]+))/);
if (heredocMatch) {
const terminator = heredocMatch[1];
const terminatorRegex = new RegExp(`\\n${escapeRegex(terminator)}\\s*$`);
return terminatorRegex.test(trimmed);
const terminator = heredocMatch[1] || heredocMatch[2] || heredocMatch[3];

// Require a quoted terminator OR a body with no command substitution
const leadingTabs = dashTrim ? '\\t*' : '';
const terminatorRegex = new RegExp(`\\n${leadingTabs}${escapeRegex(terminator)}\\s*$`);
const hasTerminator = terminatorRegex.test(trimmed);
const isQuoted = Boolean(heredocMatch[1] || heredocMatch[2]);

if (!hasTerminator) {
return false;
}

if (!isQuoted) {
// Unquoted heredoc bodies perform substitution; ensure body is clean
const body = trimmed.replace(/^.*?\n/s, '');
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The heredoc body extraction logic is incorrect. The regex /^.*?\n/s with the s flag makes . match newlines, causing the *? to match everything up to the last newline before the terminator, effectively removing the entire body except the last line.

The correct approach is to remove only the first line (without the s flag): trimmed.replace(/^.*?\n/, '')

For example, with input:

cat <<EOF > file
line1
line2
EOF

Current code extracts: EOF (just the terminator line)
Should extract: line1\nline2\nEOF

Suggested change
const body = trimmed.replace(/^.*?\n/s, '');
const body = trimmed.replace(/^.*?\n/, '');

Copilot uses AI. Check for mistakes.
if (hasCommandSubstitution(body)) {
return false;
}
}

return true;
}

return trimmed.indexOf('\n') === -1;
Expand Down
32 changes: 22 additions & 10 deletions src/hooks/lib/marker-detection.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@ const path = require('path');
const os = require('os');
const crypto = require('crypto');
const { getSetting } = require('./config-loader');
const { getProjectRoot } = require('./hook-helpers');

/**
* Marker Detection Utilities
* Shared functions for detecting agent execution markers
*/

const MAIN_SCOPE_AGENT_PRIVILEGES =
process.env.ICC_MAIN_SCOPE_AGENT === 'true' ||
getSetting('enforcement.main_scope_has_agent_privileges', false);
function isMainScopeAgentPrivileged() {
if (process.env.ICC_MAIN_SCOPE_AGENT === 'true') return true;
if (process.env.ICC_MAIN_SCOPE_AGENT === 'false') return false;
return getSetting('enforcement.main_scope_has_agent_privileges', false);
}

/**
* Get marker directory path
* @returns {string} Marker directory path
*/
function getMarkerDir() {
if (process.env.ICC_TEST_MARKER_DIR) {
return process.env.ICC_TEST_MARKER_DIR;
}
return path.join(os.homedir(), '.claude', 'tmp');
}

Expand All @@ -41,16 +47,21 @@ function ensureMarkerDir(log) {
* @param {string} projectRoot - Project root path
* @returns {string} 8-character MD5 hash
*/
function generateProjectHash(projectRoot) {
// CRITICAL: Normalize before hashing
// Ensures same path with/without trailing slash = same hash
let normalizedRoot = path.resolve(projectRoot);
function resolveProjectRoot(projectRootOrHookInput) {
// Accept either a hookInput object or a raw path string
if (projectRootOrHookInput && typeof projectRootOrHookInput === 'object' && !Array.isArray(projectRootOrHookInput)) {
return getProjectRoot(projectRootOrHookInput);
}

// Ensure no trailing slash (except root)
let normalizedRoot = path.resolve(projectRootOrHookInput || process.cwd());
if (normalizedRoot.length > 1 && normalizedRoot.endsWith(path.sep)) {
normalizedRoot = normalizedRoot.slice(0, -1);
}
return normalizedRoot;
}

function generateProjectHash(projectRootOrHookInput) {
const normalizedRoot = resolveProjectRoot(projectRootOrHookInput);
return crypto.createHash('md5').update(normalizedRoot).digest('hex').substring(0, 8);
}

Expand All @@ -61,14 +72,15 @@ function generateProjectHash(projectRoot) {
* @param {Function} log - Logger function
* @returns {boolean} true if agent context, false if main scope
*/
function isAgentContext(projectRoot, sessionId, log) {
if (MAIN_SCOPE_AGENT_PRIVILEGES) {
function isAgentContext(projectRootOrHookInput, sessionId, log) {
if (isMainScopeAgentPrivileged()) {
if (log) {
log('Config: main_scope_has_agent_privileges=true (treating main scope as agent context)');
}
return true;
}

const projectRoot = resolveProjectRoot(projectRootOrHookInput);
const projectHash = generateProjectHash(projectRoot);
const markerDir = getMarkerDir();

Expand Down
41 changes: 39 additions & 2 deletions src/hooks/pm-constraints-enforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,28 @@ To execute blocked operation:

// CRITICAL: Check tool blacklist AFTER Bash coordination check
const blacklistResult = checkToolBlacklist(tool, toolInput, 'pm', projectRoot);
if (blacklistResult.blocked) {

// Docs fast-path: if markdown is already allowed via docs/ allowlist (including parent-path fast path), skip blacklist
let markdownAllowedFastPath = false;
if (filePath && filePath.endsWith('.md')) {
const markdownValidation = validateMarkdownOutsideAllowlist(filePath, projectRoot, false);
const outsideProject = path.relative(projectRoot, filePath).startsWith('..');
const allowlistDirs = [
getSetting('paths.story_path', 'stories'),
getSetting('paths.bug_path', 'bugs'),
getSetting('paths.memory_path', 'memory'),
getSetting('paths.docs_path', 'docs'),
'agenttasks',
getSetting('paths.summaries_path', 'summaries')
];
const pathParts = path.normalize(filePath).split(path.sep);
const containsAllowlistedSegment = allowlistDirs.some((dir) => pathParts.includes(dir));
const forceAllow = ALLOW_PARENT_ALLOWLIST_PATHS && outsideProject && containsAllowlistedSegment;

markdownAllowedFastPath = markdownValidation.allowed || forceAllow;
}
Comment on lines +969 to +987
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Duplicated logic: The allowlist directory definitions and the parent path validation logic (lines 974-984) are nearly identical to lines 1119-1128. This creates a maintenance burden where changes need to be made in two places.

Consider extracting this logic into a helper function, e.g., checkMarkdownParentPathAllowlist(filePath, projectRoot) that can be called from both locations.

Copilot uses AI. Check for mistakes.

if (blacklistResult.blocked && !markdownAllowedFastPath) {
log(`Tool blocked by blacklist: ${tool} (${blacklistResult.list})`);

const blockingEnabled = getBlockingEnabled();
Expand Down Expand Up @@ -1090,7 +1111,23 @@ To execute blocked operation:
// Apply markdown validation if needed
if (shouldApplyMarkdownValidation) {
const markdownValidation = validateMarkdownOutsideAllowlist(filePath, projectRoot, false);
if (!markdownValidation.allowed) {

// If the file is outside the project, parent paths are allowed, and the path already contains an allowlisted segment, allow it
const allowParentPaths = ALLOW_PARENT_ALLOWLIST_PATHS;
const outsideProject = path.relative(projectRoot, filePath).startsWith('..');
const pathParts = path.normalize(filePath).split(path.sep);
const allowlistDirs = [
getSetting('paths.story_path', 'stories'),
getSetting('paths.bug_path', 'bugs'),
getSetting('paths.memory_path', 'memory'),
getSetting('paths.docs_path', 'docs'),
'agenttasks',
getSetting('paths.summaries_path', 'summaries')
];
const containsAllowlistedSegment = allowlistDirs.some((dir) => pathParts.includes(dir));
const forceAllow = allowParentPaths && outsideProject && containsAllowlistedSegment;

if (!markdownValidation.allowed && !forceAllow) {
log(`Markdown file outside allowlist blocked: ${filePath}`);

const blockingEnabled = getBlockingEnabled();
Expand Down
14 changes: 12 additions & 2 deletions src/hooks/project-scope-enforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { initializeHook } = require('./lib/logging');
const { extractToolInfo, allowOperation, blockOperation } = require('./lib/hook-helpers');
const { isInstallationPath } = require('./lib/path-utils');
const { isModifyingBashCommand } = require('./lib/command-validation');
const { getSetting } = require('./lib/config-loader');

function main() {
// Initialize hook with shared library function
Expand Down Expand Up @@ -96,8 +97,17 @@ function main() {
const projectRoot = getProjectRootFromHookInput(hookInput);
log(`Project root detected: ${projectRoot}`);

const envMainScopeAgent = process.env.ICC_MAIN_SCOPE_AGENT;
const mainScopeAgent =
envMainScopeAgent === 'true'
? true
: envMainScopeAgent === 'false'
? false
: getSetting('enforcement.main_scope_has_agent_privileges', false) === true;

// CRITICAL: Check project boundary FIRST (before installation check)
// Block ALL file operations outside project root (except ~/.claude/CLAUDE.md)
// Installation protection ALWAYS applies, even when main scope acts as agent.
if (filePath && (tool === 'Edit' || tool === 'Write' || tool === 'MultiEdit')) {
const isInProject = isWithinProjectBoundaries(filePath, projectRoot);
const isException = isAllowedException(filePath);
Expand Down Expand Up @@ -130,8 +140,8 @@ All work must be done within project directories:
Installation updates happen via 'make install' from project source.`, log);
}

// Log operations OUTSIDE project boundaries (but allow if intentional)
if (!isInProject) {
// If main scope has agent privileges, allow outside-project writes (agents already allowed)
if (!isInProject && !mainScopeAgent) {
log(`BLOCK: ${filePath} outside project root ${projectRoot}`);
return blockOperation(`🚫 Project boundary enforcement - stay inside ${projectRoot}

Expand Down
Loading
Loading