-`$SANDBOX/vault/journal/` — journal target for `session_end.py`
-`$SANDBOX/.cache/graphify/` — stamp/lock/log directory for `session_start.py`
-`$SANDBOX/bin/graphify` — shim that logs calls to `graphify-shim.log`, exits 0
-`$SANDBOX/bin/git` — safety shim (see below)
`PATH="$SANDBOX/bin:$PATH"` is exported so shims take priority over system binaries.
### Git shim safety
`memsearch_sync.py` reads `MEMSEARCH_DIR` from `Config.memsearch_dir` (no longer hardcoded) — but the resolved path is still absolute and bypasses `HOME=$SANDBOX`. The git shim is the only protection against the hook committing or pushing to the real memsearch repository.
The shim at `$SANDBOX/bin/git`:
- Scans every argument for the string `/home/jared/.memsearch`
- If found: logs the blocked call to `$SANDBOX/git-shim.log` and exits 0
- If not found: `exec /usr/bin/git "$@"` (passthrough)
**Critical**: the log path is baked into the shim file at write-time (absolute literal), not
passed only via env var. This ensures the log is captured even inside session-end's
`( ... ) || true` subshell.
The shim emits **nothing** to stdout or stderr — only writes to the log file. This matters
because `memsearch_sync.py` runs `git diff --cached --quiet` without redirecting stdout, and any
shim chatter would appear in captured stdout and corrupt golden fixtures.
### Normalization rules
Applied in this order (order matters: timestamp before date-only avoids partial corruption):
1. ISO 8601 timestamps `YYYY-MM-DDTHH:MM:SSZ` → `__TIMESTAMP__`
2. Date-only `YYYY-MM-DD` → `__DATE__`
3. Sandbox temp path `$SANDBOX` → `__SANDBOX__`
4.`cwd=<path>` log values → `cwd=__CWD__` (varies by machine and harness invocation dir)
5. Real home path `/home/jared` → `__REAL_HOME__`
## Scenarios
### session-start group
| Scenario | Setup | What it tests |
|----------|-------|---------------|
| `session-start-fresh` | Stamp file exists (fresh mtime) | Rebuild NOT triggered; log says "rebuild not needed" |
| `session-start-stale` | No stamp file | Rebuild triggered via nohup; graphify shim called with vault path |
Side effects captured: `memory-hook-log-delta.txt` (lines added to `/tmp/memory-hook.log`),
`graphify-shim-args.txt` (stale only — polled up to 5 seconds for background call).
### session-context group
| Scenario | Setup | What it tests |
|----------|-------|---------------|
| `session-context-first-with-graph` | Real git repo + `graphify-out/graph.json` | First prompt injects graph path in additionalContext |
| `session-context-reentry` | Flag file pre-existing | Returns `{}` immediately (no-op on re-entry) |
| `session-context-no-graph` | Real git repo, no `graphify-out/graph.json` | Returns empty additionalContext |
Side effects captured: `flag-file-created.txt` (yes/no for all session-context scenarios).
### post-tool-use-write group
| Scenario | Setup | What it tests |
|----------|-------|---------------|
| `post-tool-use-vault-md` | Stamp exists, vault/notes dir | Touch file written, stamp deleted, log entry created |
| `post-tool-use-non-md` | vault/notes dir | Non-.md file → all guards fire, no side effects |
| `post-tool-use-outside-vault` | (none) | Outside-vault path → no side effects |
Side effects captured: `touch-file.txt`, `post-tool-use-log.txt`, `stamp-deleted.txt` (all
post-tool-use scenarios; absent/empty for non-md and outside-vault);
`noops.txt` confirmation for non-md and outside-vault.
2.**session-start cwd in log lines is normalized to `__CWD__`.** The hook logs
`cwd=$(pwd)` (shell invocation directory, not the JSON `cwd` field). This varies by
machine and harness run location, so it is masked. The Python port should produce the
same log format with the actual cwd; the harness normalization will mask it identically.
3.**session-context stdout JSON must match exactly.** The fixture captures output of
`json.dumps(...)` with no extra whitespace. The Python port must serialize with the
same key order and no trailing newline inside the JSON value. Diff will fail on
whitespace differences even if the data is semantically identical.
4.**Journal filename is date-based (`YYYY-MM-DD.md`).** Masked to `__DATE__` in
normalized content. The harness finds the journal file by glob rather than recomputing
today's date, so it works across midnight boundaries.
5.**session-end `**Project:** unknown`** — session-end hooks run from `/tmp` with no git
repo and `CLAUDE_PROJECT_DIR` unset. The Python port should produce identical output.
## Replaying session-end scenarios safely
`memsearch_sync.py` reads the memsearch path from `Config.memsearch_dir` (not hardcoded), but the resolved path is still absolute and bypasses the sandbox. The sequence to follow: