Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
40f018b
Add agent-scope: task-scoped write permissions for AI agents
Bojan131 Apr 21, 2026
0a2e45b
remove commit blocker and ci
Bojan131 Apr 21, 2026
1db9316
update
Bojan131 Apr 21, 2026
e2e82ec
Add one-shot task-onboarding flow with top-of-turn marker check
Bojan131 Apr 21, 2026
c5bb06d
agent-scope: add Claude Code hook parity + cross-agent rule files
Bojan131 Apr 21, 2026
abf6d5b
agent-scope: make pnpm task start an interactive CLI wizard
Bojan131 Apr 22, 2026
d30b798
agent-scope: replace --chat with --smart — AI-driven onboarding with …
Bojan131 Apr 22, 2026
6926196
agent-scope: allow agent-run pnpm task create to persist the manifest
Bojan131 Apr 22, 2026
499757e
agent-scope: single-Enter submission for smart-mode description
Bojan131 Apr 22, 2026
ba7da99
agent-scope: one-question / two-option plan-mode prompts (simpler UX)
Bojan131 Apr 22, 2026
562e82d
agent-scope: make smart onboarding the only pnpm task start mode
Bojan131 Apr 22, 2026
e0d05bf
agent-scope: shorter, more human CLI copy for `pnpm task start`
Bojan131 Apr 22, 2026
6c5a7cc
agent-scope: tighten CLI copy and plan-mode prompt UX
Bojan131 Apr 22, 2026
0d393d3
agent-scope: fix two afterShell bugs (fd-redirect parse + manifest re…
Bojan131 Apr 22, 2026
5b95fcf
update readme
Bojan131 Apr 22, 2026
22a3aeb
Merge remote-tracking branch 'origin/v10-rc' into agent-non-collision
Bojan131 Apr 22, 2026
82fa43c
clean the code
Bojan131 Apr 22, 2026
db059b3
update
Bojan131 Apr 22, 2026
45ad6aa
update
Bojan131 Apr 22, 2026
fd48eb0
Merge remote-tracking branch 'origin/main' into agent-non-collision
Bojan131 Apr 27, 2026
9284948
update system to use dkg
Bojan131 Apr 27, 2026
d6c63a8
agent-scope: zero-config onboarding for new clones
Bojan131 Apr 28, 2026
b6af2bb
mcp-dkg: auto-create the configured paranet on first connect
Bojan131 Apr 28, 2026
752ae2e
Merge remote-tracking branch 'origin/main' into agent-non-collision
Bojan131 Apr 29, 2026
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
164 changes: 164 additions & 0 deletions .claude/hooks/scope-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env node
// Claude Code PreToolUse hook for write-class tools (Write/Edit/MultiEdit/
// NotebookEdit). Blocks writes to:
// 1. Hardcoded protected paths (always, unless bootstrap mode is on)
// 2. Paths outside the active task's allowed/exemption globs
//
// Same policy as the Cursor preToolUse hook — only the I/O envelope
// differs. All decisions go through agent-scope/lib so Cursor and Claude
// Code stay byte-for-byte identical on rule semantics.
//
// Claude Code I/O contract:
// stdin: JSON { session_id, hook_event_name, tool_name, tool_input, ... }
// stdout: JSON { hookSpecificOutput: {
// hookEventName: "PreToolUse",
// permissionDecision: "deny" | "allow" | "ask",
// permissionDecisionReason: "..." } }
// exit 0 always for clean handling (non-zero would error out the agent).

import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const scopeUrl = pathToFileURL(resolve(__dirname, '../../agent-scope/lib/scope.mjs')).href;
const logUrl = pathToFileURL(resolve(__dirname, '../../agent-scope/lib/log.mjs')).href;
const denialUrl = pathToFileURL(resolve(__dirname, '../../agent-scope/lib/denial.mjs')).href;
const {
resolveRepoRoot, resolveActiveTaskId, loadTask, checkPath,
normalizeToRepoPath, checkNodeVersion, checkProtected,
} = await import(scopeUrl);
const { logDenial, logDecision } = await import(logUrl);
const {
buildPreToolUseDenial, buildLoadErrorDenial,
} = await import(denialUrl);

try { checkNodeVersion(); } catch (e) {
process.stderr.write(e.message + '\n');
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
},
}));
process.exit(0);
}

function emit(decision, reason) {
const out = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: decision,
},
};
if (reason) out.hookSpecificOutput.permissionDecisionReason = reason;
process.stdout.write(JSON.stringify(out));
process.exit(0);
}

const allow = () => emit('allow');
const deny = (msg) => emit('deny', msg);

function readStdin() {
try { return readFileSync(0, 'utf8'); } catch { return ''; }
}

function extractTargetPath(toolInput) {
if (!toolInput || typeof toolInput !== 'object') return null;
return (
toolInput.path ||
toolInput.target_file ||
toolInput.file_path ||
toolInput.filepath ||
toolInput.notebook_path ||
toolInput.target_notebook ||
null
);
}

async function main() {
const raw = readStdin();
if (!raw) return allow();

let payload;
try { payload = JSON.parse(raw); } catch { return allow(); }

const toolName = payload.tool_name || payload.toolName || payload.tool || '';
const toolInput = payload.tool_input || payload.toolInput || payload.input || {};
const sessionId = payload.session_id || payload.sessionId || null;

const GUARDED = /^(Write|Edit|MultiEdit|NotebookEdit|StrReplace|Delete|EditNotebook)$/;
if (!GUARDED.test(toolName)) return allow();

const targetPath = extractTargetPath(toolInput);
if (!targetPath) return allow();

const root = resolveRepoRoot();
const rel = normalizeToRepoPath(root, targetPath);

if (checkProtected(rel, root) === 'deny') {
const { id: tid } = resolveActiveTaskId(root);
logDenial(root, {
event: 'preToolUse.protected',
tool: toolName,
path: rel,
task: tid,
sessionId,
agent: 'claude-code',
});
const { message } = buildPreToolUseDenial({
tool: toolName, deniedPath: rel, decision: 'protected',
task: null, taskId: tid, root,
});
return deny(message);
}

const { id: taskId, source: taskSource } = resolveActiveTaskId(root);
if (!taskId) return allow();

let task;
try { task = loadTask(root, taskId); }
catch (e) {
const { message } = buildLoadErrorDenial({ taskId, error: e.message });
return deny(message);
}

const decision = checkPath(task, rel, root);

logDecision(root, {
event: 'preToolUse',
tool: toolName,
decision,
path: rel,
task: taskId,
taskSource,
sessionId,
agent: 'claude-code',
});

if (decision === 'allow' || decision === 'exempt') return allow();

logDenial(root, {
event: 'preToolUse.deny',
tool: toolName,
path: rel,
decision,
task: taskId,
taskSource,
sessionId,
agent: 'claude-code',
});

const { message } = buildPreToolUseDenial({
tool: toolName, deniedPath: rel, decision,
task, taskId, root,
});
return deny(message);
}

main().catch(err => {
process.stderr.write(`scope-guard hook error: ${err?.message || err}\n`);
allow();
});
145 changes: 145 additions & 0 deletions .claude/hooks/session-start.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/env node
// Claude Code SessionStart hook. Mirrors the Cursor sessionStart hook:
// injects the active scope (or a bootstrap warning) into the agent's
// initial context. Source of truth is the local DKG daemon — the union
// of `tasks:scopedToPath` across every `tasks:Task` whose status is
// `in_progress` and which is `prov:wasAttributedTo` this agent.

import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const scopeUrl = pathToFileURL(resolve(__dirname, '../../agent-scope/lib/scope.mjs')).href;
const {
resolveRepoRoot, resolveActiveScope, checkNodeVersion, isBootstrapActive,
} = await import(scopeUrl);

try { checkNodeVersion(); } catch (e) {
process.stderr.write(e.message + '\n');
process.stdout.write('{}');
process.exit(0);
}

function emit(context) {
if (!context) { process.stdout.write('{}'); process.exit(0); }
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: context,
},
}));
process.exit(0);
}

function readStdin() {
try { readFileSync(0, 'utf8'); } catch { /* ignore */ }
}

async function main() {
readStdin();
const root = resolveRepoRoot();
const scope = await resolveActiveScope({ root, force: true });
const bootstrap = isBootstrapActive(root);

const header = [];
if (bootstrap) {
header.push(
'# agent-scope: BOOTSTRAP MODE ACTIVE',
'',
'Hardcoded path protection is currently DISABLED because a human has enabled',
'bootstrap mode (token file or env var). Writes to system files are permitted.',
'',
'If you are not explicitly working on improving agent-scope itself, ask the',
'user to disable bootstrap mode before proceeding:',
' rm agent-scope/.bootstrap-token',
'',
);
}

if (scope.reason !== 'ok') {
if (!bootstrap) {
if (scope.reason === 'daemon-unreachable' || scope.reason === 'configuration-error') {
return emit([
'# agent-scope: scope source unavailable',
'',
`Scope can't be resolved right now (${scope.reason}). Only the hardcoded`,
'protected path list is enforced; everything else is writable.',
scope.diagnostic ? '' : null,
scope.diagnostic ? `Diagnostic: ${scope.diagnostic}` : null,
].filter((l) => l !== null).join('\n'));
}
return emit(null);
}
return emit(header.concat([
'# agent-scope: no in-progress task',
'',
'Bootstrap is active but no `tasks:Task` is currently in_progress for this',
'agent. System files are writable. When the protected work is done, run:',
' rm agent-scope/.bootstrap-token',
]).join('\n'));
}

const tasks = Array.isArray(scope.tasks) ? scope.tasks : [];
const allowedPositive = (scope.allowed || []).filter((p) => !p.startsWith('!'));
const allowedNegative = (scope.allowed || []).filter((p) => p.startsWith('!'));
const exemptionsPositive = (scope.exemptions || []).filter((p) => !p.startsWith('!'));
const exemptionsNegative = (scope.exemptions || []).filter((p) => p.startsWith('!'));

const heading = tasks.length === 1
? `# agent-scope: active task — ${tasks[0].uri}`
: `# agent-scope: ${tasks.length} active in-progress tasks`;

const lines = header.concat([heading, '']);
if (tasks.length === 1) {
const t = tasks[0];
lines.push(`**Task:** ${t.title || '(untitled)'}`);
if (t.assignee) lines.push(`**Assignee:** ${t.assignee}`);
} else {
lines.push('## In-progress tasks');
for (const t of tasks) {
lines.push(`- \`${t.uri}\` — ${t.title || '(untitled)'}`);
}
}
if (scope.agentUri) lines.push(`**Agent:** ${scope.agentUri}`);
if (scope.projectId) lines.push(`**Project:** ${scope.projectId}`);
lines.push('');

lines.push(
'## You may modify files matching the union of these globs:',
...(allowedPositive.length ? allowedPositive.map((p) => `- \`${p}\``) : ['- (nothing — every in-progress task has empty `tasks:scopedToPath`)']),
);
if (exemptionsPositive.length) {
lines.push('', '## Always allowed (build artefacts, lockfiles):');
for (const p of exemptionsPositive) lines.push(`- \`${p}\``);
}
if (allowedNegative.length || exemptionsNegative.length) {
lines.push('', '## Explicitly denied (even if they look in-scope):');
for (const p of [...allowedNegative, ...exemptionsNegative]) lines.push(`- \`${p}\``);
}

lines.push(
'',
'## Rules',
'- You may **read** any file in the repo.',
'- You may **write** only files matching the patterns above.',
'- System files (`.cursor/hooks/**`, `.claude/hooks/**`, `agent-scope/lib/**`, etc.) are hardcode-protected regardless of task.' + (bootstrap ? ' (currently bypassed by bootstrap mode)' : ''),
'- The allow-list is computed live from the local DKG daemon. To extend scope:',
' call `dkg_add_task` with `status: "in_progress"` and a `scopedToPath` glob covering',
' the new path; the cache will pick it up within ~5s.',
'- When a task is done, call `dkg_update_task_status({ taskUri, status: "done" })`.',
' The next scope read will drop its globs from the union automatically.',
'- A Claude Code hook enforces this on every Write/Edit/Delete; pre-Bash blocks',
' destructive shell commands on denied paths; post-Bash reverts anything that',
' slipped through.',
);

emit(lines.filter((l) => l !== null).join('\n'));
}

main().catch((err) => {
process.stderr.write(`session-start hook error: ${err?.message || err}\n`);
emit(null);
});
Loading
Loading