Add file_locked frontmatter guard hook for hand-tuned files #292

Closed
opened 2026-08-06 18:29:54 +00:00 by jared · 3 comments
Owner

Context

os-sdlc implement (and agents generally) sometimes edit hand-tuned files unprompted. Assessed 2026-08-06 via multi-agent review (hook mechanics, cc-os hook survey, Codex, simplifier + premortem perspectives): verdict is build it, narrow scope.

Design: a PreToolUse hook (first in cc-os) matching Write|Edit|NotebookEdit reads the target file's on-disk YAML frontmatter; if metadata.file_locked: true, return permissionDecision: ask with a reason so edits must be presented as before/after diffs and approved. PreToolUse hooks apply to subagent tool calls too.

Constraints from assessment:

  1. Scope initially to SKILL.md and agents/*.md with cheap early-exits (extension, file-exists, --- prefix); expand to other .md/Obsidian later.
  2. Fail CLOSED: any frontmatter parse error on an in-scope file -> ask, never silent allow.
  3. Hook reads the CURRENT on-disk file so an edit can't strip its own lock; prompt must call out when an edit removes file_locked.
  4. Define headless behavior: in non-interactive os-sdlc pipeline runs, ask must resolve to a clear deny+reason surfaced to the agent, never a silent hang.
  5. Known limitation: Bash-mediated writes (sed -i, heredocs) bypass Write/Edit hooks — guardrail, not a security boundary; Bash restriction for pipeline agents is a follow-up.

Reuse: plugins/os-aidd-lint/hooks/post_tool_use.rb as Ruby hook template; os-doc-hygiene scanner.py/patch_applier.py have frontmatter parsers. Placement (new plugin vs os-status) and mechanism choice vs settings.json permission rules to be recorded as an ADR at implementation time.

## Context os-sdlc implement (and agents generally) sometimes edit hand-tuned files unprompted. Assessed 2026-08-06 via multi-agent review (hook mechanics, cc-os hook survey, Codex, simplifier + premortem perspectives): verdict is build it, narrow scope. Design: a PreToolUse hook (first in cc-os) matching Write|Edit|NotebookEdit reads the target file's on-disk YAML frontmatter; if `metadata.file_locked: true`, return `permissionDecision: ask` with a reason so edits must be presented as before/after diffs and approved. PreToolUse hooks apply to subagent tool calls too. Constraints from assessment: 1. Scope initially to SKILL.md and agents/*.md with cheap early-exits (extension, file-exists, `---` prefix); expand to other .md/Obsidian later. 2. Fail CLOSED: any frontmatter parse error on an in-scope file -> ask, never silent allow. 3. Hook reads the CURRENT on-disk file so an edit can't strip its own lock; prompt must call out when an edit removes file_locked. 4. Define headless behavior: in non-interactive os-sdlc pipeline runs, ask must resolve to a clear deny+reason surfaced to the agent, never a silent hang. 5. Known limitation: Bash-mediated writes (sed -i, heredocs) bypass Write/Edit hooks — guardrail, not a security boundary; Bash restriction for pipeline agents is a follow-up. Reuse: plugins/os-aidd-lint/hooks/post_tool_use.rb as Ruby hook template; os-doc-hygiene scanner.py/patch_applier.py have frontmatter parsers. Placement (new plugin vs os-status) and mechanism choice vs settings.json permission rules to be recorded as an ADR at implementation time.
Author
Owner

Direction revised after grill session 2026-08-06 — this comment supersedes conflicting parts of the description:

  1. SCOPE: global, no path allowlist. Hook matches Write|Edit|NotebookEdit; script early-exits unless target ends in .md; frontmatter is the sole scope — only files with metadata.file_locked: true in valid frontmatter (line 1 is exactly ---, block ends at next --- line; bound scan at 100 lines/8KB) trigger permissionDecision ask. Opt-in means false positives impossible by construction.
  2. PLACEMENT: hook registered in os-status's own hooks.json (global install = activation; nothing to install separately). Record via ADR: os-status charter expands from deterministic checks to checks + deterministic guards. If the planned os-status+os-shortcuts merge into an 'os' plugin lands first, the hook lands there instead.
  3. CUT the 'restrict Bash for pipeline agents' follow-up (speculative hardening — ask is a question, not a wall; workaround behavior is documented for deny walls, not confirmation prompts). KEEP the one-line factual note that Bash-mediated writes are not covered by Write/Edit hooks.
  4. UNCHANGED: fail-closed (malformed/unclosed frontmatter after an opening --- on an in-scope file -> ask); hook reads CURRENT on-disk file so an edit cannot strip its own lock, and the prompt must call out lock removal; headless runs: ask must degrade to a clear deny+reason surfaced to the agent, never a hang.
  5. The hook is a dumb standalone script: no dependency on any skill, registry doc, or plugin logic. Related follow-on tickets: frontmatter registry+skill, and the os plugin merge (see tracker).
    Execution order across the three tickets: this one first.
Direction revised after grill session 2026-08-06 — this comment supersedes conflicting parts of the description: 1. SCOPE: global, no path allowlist. Hook matches Write|Edit|NotebookEdit; script early-exits unless target ends in .md; frontmatter is the sole scope — only files with metadata.file_locked: true in valid frontmatter (line 1 is exactly ---, block ends at next --- line; bound scan at 100 lines/8KB) trigger permissionDecision ask. Opt-in means false positives impossible by construction. 2. PLACEMENT: hook registered in os-status's own hooks.json (global install = activation; nothing to install separately). Record via ADR: os-status charter expands from deterministic checks to checks + deterministic guards. If the planned os-status+os-shortcuts merge into an 'os' plugin lands first, the hook lands there instead. 3. CUT the 'restrict Bash for pipeline agents' follow-up (speculative hardening — ask is a question, not a wall; workaround behavior is documented for deny walls, not confirmation prompts). KEEP the one-line factual note that Bash-mediated writes are not covered by Write/Edit hooks. 4. UNCHANGED: fail-closed (malformed/unclosed frontmatter after an opening --- on an in-scope file -> ask); hook reads CURRENT on-disk file so an edit cannot strip its own lock, and the prompt must call out lock removal; headless runs: ask must degrade to a clear deny+reason surfaced to the agent, never a hang. 5. The hook is a dumb standalone script: no dependency on any skill, registry doc, or plugin logic. Related follow-on tickets: frontmatter registry+skill, and the os plugin merge (see tracker). Execution order across the three tickets: this one first.
Author
Owner

Work started 2026-08-06, in-session on branch main (direct commit, no PR). Implementing as python hook plugins/os-status/hooks/pre_tool_use.py per the revised-direction comment above.

Work started 2026-08-06, in-session on branch main (direct commit, no PR). Implementing as python hook plugins/os-status/hooks/pre_tool_use.py per the revised-direction comment above.
Author
Owner

Resolution

Done: Implemented file_locked frontmatter guard: PreToolUse hook plugins/os-status/hooks/pre_tool_use.py registered in os-status hooks.json (matcher Write|Edit|MultiEdit|NotebookEdit). Opt-in metadata.file_locked: true on any .md (case-insensitive extension) returns permissionDecision ask; fail-closed on opened-but-unparseable frontmatter; lock decision reads on-disk file only; ask reason calls out lock removal (Write/Edit/MultiEdit). Charter expansion recorded as ADR-0104 (amends ADR-0022); os-status CLAUDE.md + invariants 9-13 updated; plugin cache refreshed.

Evidence: Commit 079f5cc on main. Tests: python3 tests/pre_tool_use_test.py = 31 passed; python3 tests/hook_test.py = 80 passed (unchanged). Two-axis code review (standards + spec) completed; all findings fixed (.MD suffix bypass, tab-indent detection, case-sensitive lock value, doc sync).

Follow-ups: #294 (merge os-status+os-shortcuts into os plugin, noun-first naming ADR) then #293 (frontmatter registry doc + CRUD skill) — already filed, execution order #294 before #293. No other follow-ups.

## Resolution **Done:** Implemented file_locked frontmatter guard: PreToolUse hook plugins/os-status/hooks/pre_tool_use.py registered in os-status hooks.json (matcher Write|Edit|MultiEdit|NotebookEdit). Opt-in metadata.file_locked: true on any .md (case-insensitive extension) returns permissionDecision ask; fail-closed on opened-but-unparseable frontmatter; lock decision reads on-disk file only; ask reason calls out lock removal (Write/Edit/MultiEdit). Charter expansion recorded as ADR-0104 (amends ADR-0022); os-status CLAUDE.md + invariants 9-13 updated; plugin cache refreshed. **Evidence:** Commit 079f5cc on main. Tests: python3 tests/pre_tool_use_test.py = 31 passed; python3 tests/hook_test.py = 80 passed (unchanged). Two-axis code review (standards + spec) completed; all findings fixed (.MD suffix bypass, tab-indent detection, case-sensitive lock value, doc sync). **Follow-ups:** #294 (merge os-status+os-shortcuts into os plugin, noun-first naming ADR) then #293 (frontmatter registry doc + CRUD skill) — already filed, execution order #294 before #293. No other follow-ups.
jared closed this issue 2026-08-06 19:47:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#292
No description provided.