---
phase: 01-core-hardening
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - src/ztlctl/services/check.py
  - src/ztlctl/infrastructure/database/schema.py
  - src/ztlctl/plugins/builtins/git.py
  - src/ztlctl/commands/serve.py
  - src/ztlctl/services/workflow.py
  - tests/plugins/test_git_plugin.py
autonomous: true
requirements: [HARD-01, HARD-07]

must_haves:
  truths:
    - "backup_retention_days config setting is enforced — backups older than the configured days are pruned"
    - "VEC_CREATE_SQL dead code is removed from schema.py"
    - "import json is at module top level in check.py, not inside the per-file loop"
    - "graph materialize is called after check --rebuild"
    - "Git commit messages are sanitized: newlines and null bytes stripped from title/content_id"
    - "MCP HTTP transport shows a warning about no authentication"
    - "Copier trust flag is verified and documented"
  artifacts:
    - path: "src/ztlctl/services/check.py"
      provides: "backup_retention_days enforcement, import json moved, post-rebuild materialize"
      contains: "backup_retention_days"
    - path: "src/ztlctl/infrastructure/database/schema.py"
      provides: "VEC_CREATE_SQL removed"
    - path: "src/ztlctl/plugins/builtins/git.py"
      provides: "Commit message sanitization"
      contains: "_sanitize_for_commit"
    - path: "src/ztlctl/commands/serve.py"
      provides: "HTTP transport warning"
      contains: "WARNING"
  key_links:
    - from: "src/ztlctl/services/check.py"
      to: "src/ztlctl/config/models.py"
      via: "reads backup_retention_days from CheckConfig"
      pattern: "backup_retention_days"
    - from: "src/ztlctl/services/check.py"
      to: "src/ztlctl/services/graph.py"
      via: "calls GraphService.materialize_metrics() after rebuild"
      pattern: "materialize_metrics"
---

<objective>
Fix all tech debt items from CONCERNS.md (HARD-01) and security issues (HARD-07). These are isolated, surgical fixes across multiple files with no interdependencies.

Purpose: Eliminate misleading config settings, dead code, and security gaps before test coverage is lifted.
Output: 5 tech debt fixes in check.py/schema.py + 3 security hardening fixes in git.py/serve.py/workflow.py.
</objective>

<execution_context>
@/Users/shparki/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shparki/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/codebase/CONCERNS.md
@.planning/phases/01-core-hardening/01-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs. -->

From src/ztlctl/config/models.py:
```python
class CheckConfig(BaseModel):
    backup_max_count: int = 10
    backup_retention_days: int = 30  # DECLARED BUT NEVER ENFORCED
```

From src/ztlctl/services/check.py:
```python
class CheckService(BaseService):
    def rebuild(self) -> ServiceResult: ...  # line 139, sequential file reads
    def _prune_backups(self, backup_dir: Path) -> None: ...  # line 309, only count-based
    def _backup_db(self) -> Path: ...  # line 296
```

From src/ztlctl/infrastructure/database/schema.py:
```python
VEC_CREATE_SQL = "..."  # line ~153, dead code — VectorService.ensure_table() creates at runtime
```

From src/ztlctl/plugins/builtins/git.py:
```python
# lines 72, 88: commit message constructed with unsanitized title
message = f"ztlctl: create {content_id} — {title}"
```

From src/ztlctl/commands/serve.py:
```python
# HTTP transport has no warning about lack of auth
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Fix tech debt — backup retention, dead code, import, post-rebuild materialize</name>
  <files>src/ztlctl/services/check.py, src/ztlctl/infrastructure/database/schema.py</files>
  <read_first>
    - src/ztlctl/services/check.py (full file — _prune_backups at line 309, rebuild at line 139, import json location)
    - src/ztlctl/infrastructure/database/schema.py (VEC_CREATE_SQL constant)
    - src/ztlctl/config/models.py (CheckConfig with backup_retention_days field)
    - src/ztlctl/services/graph.py (GraphService.materialize_metrics signature)
  </read_first>
  <action>
    **In src/ztlctl/services/check.py:**

    1. Move `import json` from inside the per-file loop body (around line 198) to the module-level imports at the top of the file.

    2. In `_prune_backups(self, backup_dir: Path)` (line 309), add age-based pruning AFTER the existing count-based pruning:
       ```python
       from datetime import datetime, timedelta

       # Existing count-based pruning stays as-is
       # ... existing code ...

       # Age-based pruning (enforce backup_retention_days)
       retention_days = self._vault.settings.check.backup_retention_days
       if retention_days > 0:
           cutoff = datetime.now() - timedelta(days=retention_days)
           for backup in list(backups):
               mtime = datetime.fromtimestamp(backup.stat().st_mtime)
               if mtime < cutoff:
                   backup.unlink()
                   backups.remove(backup)
       ```
       Import `datetime` and `timedelta` at the module top level (add to existing imports if not present).

    3. At the end of the `rebuild()` method (line 139), after all file processing and DB writes complete but before returning the ServiceResult, add a call to materialize graph metrics:
       ```python
       # Refresh PageRank after full rebuild
       from ztlctl.services.graph import GraphService  # lazy local import
       GraphService(self._vault).materialize_metrics()
       ```
       Use the lazy local import pattern (6 precedents in codebase per CONVENTIONS.md).

    **In src/ztlctl/infrastructure/database/schema.py:**

    4. Remove the `VEC_CREATE_SQL` constant entirely (around line 153). It is dead code — `VectorService.ensure_table()` creates the vec table at runtime using the config dimension. Search for any imports of `VEC_CREATE_SQL` in the codebase first and remove those too (there should be none).
  </action>
  <verify>
    <automated>uv run pytest tests/ -x -q && uv run ruff check src/ztlctl/services/check.py src/ztlctl/infrastructure/database/schema.py && uv run mypy src/ztlctl/services/check.py src/ztlctl/infrastructure/database/schema.py --strict</automated>
  </verify>
  <acceptance_criteria>
    - src/ztlctl/services/check.py contains `import json` in the first 30 lines (module-level), not inside any method body
    - src/ztlctl/services/check.py `_prune_backups` method contains `backup_retention_days`
    - src/ztlctl/services/check.py `_prune_backups` method contains `timedelta(days=`
    - src/ztlctl/services/check.py `rebuild` method contains `materialize_metrics()`
    - src/ztlctl/infrastructure/database/schema.py does NOT contain `VEC_CREATE_SQL`
    - `uv run pytest tests/ -x -q` exits 0
    - `uv run ruff check src/ztlctl/services/check.py src/ztlctl/infrastructure/database/schema.py` exits 0
  </acceptance_criteria>
  <done>4 tech debt items fixed: backup_retention_days enforced, VEC_CREATE_SQL removed, import json moved to module level, graph materialize called after rebuild. All tests pass.</done>
</task>

<task type="auto">
  <name>Task 2: Fix security issues — git sanitization, HTTP warning, Copier trust</name>
  <files>src/ztlctl/plugins/builtins/git.py, src/ztlctl/commands/serve.py, src/ztlctl/services/workflow.py, tests/plugins/test_git_plugin.py</files>
  <read_first>
    - src/ztlctl/plugins/builtins/git.py (lines 70-90 — commit message construction)
    - src/ztlctl/commands/serve.py (transport parameter handling)
    - src/ztlctl/services/workflow.py (copier.run_copy and copier.run_update calls, trust parameter)
    - tests/plugins/test_git_plugin.py (existing test patterns)
  </read_first>
  <action>
    **In src/ztlctl/plugins/builtins/git.py:**

    1. Add a helper function near the top of the file (after imports, before the class):
       ```python
       def _sanitize_for_commit(text: str) -> str:
           """Remove newlines and null bytes from user-supplied text for commit messages."""
           return text.replace("\n", " ").replace("\r", " ").replace("\0", "")
       ```

    2. Find all commit message construction sites (around lines 72 and 88 where `content_id` and `title` are used in f-strings for `subprocess.run(["git", "commit", "-m", message])`). Wrap `content_id` and `title` with `_sanitize_for_commit()`:
       ```python
       message = f"ztlctl: create {_sanitize_for_commit(content_id)} — {_sanitize_for_commit(title)}"
       ```
       Apply to ALL commit message construction sites in the file.

    **In src/ztlctl/commands/serve.py:**

    3. Find where the `transport` parameter is used to start the server. Add a warning BEFORE the server starts when transport is "http" or "streamable-http":
       ```python
       if transport in ("http", "streamable-http"):
           click.echo(
               "WARNING: HTTP transport has no authentication. "
               "Only use on trusted local networks.",
               err=True,
           )
       ```
       Import `click` if not already imported (it should be since this is a Click command file).

    **In src/ztlctl/services/workflow.py:**

    4. Audit the `copier.run_copy()` and `copier.run_update()` calls. Check if `trust` parameter is being passed. If `trust=True` is set, change to `trust=False`. If `trust` is not set, verify Copier's default. Add a comment documenting the trust behavior:
       ```python
       # SECURITY: trust=False prevents Copier template hooks from executing
       # without explicit user consent. See HARD-07.
       ```

    **In tests/plugins/test_git_plugin.py:**

    5. Add a test for commit message sanitization:
       ```python
       def test_sanitize_for_commit_strips_newlines():
           from ztlctl.plugins.builtins.git import _sanitize_for_commit
           assert _sanitize_for_commit("hello\nworld") == "hello world"
           assert _sanitize_for_commit("hello\rworld") == "hello world"
           assert _sanitize_for_commit("hello\0world") == "helloworld"
           assert _sanitize_for_commit("clean text") == "clean text"
       ```
  </action>
  <verify>
    <automated>uv run pytest tests/plugins/test_git_plugin.py -x -v && uv run ruff check src/ztlctl/plugins/builtins/git.py src/ztlctl/commands/serve.py src/ztlctl/services/workflow.py && uv run mypy src/ztlctl/plugins/builtins/git.py src/ztlctl/commands/serve.py --strict</automated>
  </verify>
  <acceptance_criteria>
    - src/ztlctl/plugins/builtins/git.py contains `def _sanitize_for_commit(text: str) -> str:`
    - src/ztlctl/plugins/builtins/git.py contains `_sanitize_for_commit(` in commit message construction
    - src/ztlctl/commands/serve.py contains `WARNING: HTTP transport has no authentication`
    - tests/plugins/test_git_plugin.py contains `def test_sanitize_for_commit`
    - `uv run pytest tests/plugins/test_git_plugin.py -x -v` exits 0
    - `uv run ruff check src/ztlctl/plugins/builtins/git.py src/ztlctl/commands/serve.py` exits 0
  </acceptance_criteria>
  <done>3 security fixes applied: git commit messages sanitized, HTTP transport warning added, Copier trust flag audited. Sanitization test added and passing.</done>
</task>

</tasks>

<verification>
- `uv run pytest tests/ -x -q` — full suite passes
- `uv run ruff check .` — no lint issues
- `uv run mypy src/ --strict` — no type errors
- Grep confirms: `VEC_CREATE_SQL` absent from schema.py, `_sanitize_for_commit` present in git.py, `WARNING` present in serve.py, `backup_retention_days` enforced in check.py
</verification>

<success_criteria>
- 5 CONCERNS.md tech debt items fixed (backup_retention_days, VEC_CREATE_SQL, import json, post-rebuild materialize, graph auto-materialize)
- 3 security items fixed (git sanitization, HTTP warning, Copier trust audit)
- All existing tests continue to pass
- New git sanitization test passes
</success_criteria>

<output>
After completion, create `.planning/phases/01-core-hardening/01-02-SUMMARY.md`
</output>
