# How the SessionStart Hook Initializes Sessions in Caveman: Execution Flow and Architecture

> Understand how the SessionStart hook initializes sessions in Caveman. Learn its execution flow and architecture for preloading configuration and data into session context.

- Repository: [Julius Brussee/caveman](https://github.com/JuliusBrussee/caveman)
- Tags: architecture
- Published: 2026-08-22

---

**The SessionStart hook in Caveman executes once at session creation to preload configuration and data into the session context before any user prompt is processed, merging its `hookSpecificOutput` JSON field into the atomic session state.**

In the **JuliusBrussee/caveman** repository, the SessionStart hook serves as the primary entry point for session initialization, allowing developers to inject pre-computed data into the conversation context without per-turn overhead. This hook triggers immediately after the proxy generates a fresh session identifier and before the first user prompt reaches the model, making it ideal for expensive operations like loading configuration files or caching remote resources according to the source code.

## SessionStart Hook Execution Flow

### Session Initialization and Event Detection

When Caveman’s proxy router boots a new session, it first creates a **fresh session identifier** used for logging, telemetry, and cache keys. In [`proxy/internal/gateway/server.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/gateway/server.go), the gateway validates the `SessionMarkerKey` to detect the `session_start` event and forward it to the native-hook transport layer. This validation ensures that session-specific logic only runs for genuine new session events, not for ongoing conversation turns.

### Hook Invocation and Context Injection

Once the event is detected, the **native hook runtime** spawns the external hook process via the command `caveman native-hook <Event>`, passing the event JSON on stdin and reading the hook’s reply on stdout. The `Run` function in [`proxy/internal/nativehook/hook.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/nativehook/hook.go) executes this process with a strict **250 ms deadline**. The hook must return a JSON object containing a `hookSpecificOutput` field, which the runtime extracts and merges into the session’s initial context (`SessionContext`). As noted in [`proxy/internal/store/learn.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/store/learn.go), this mechanism allows "SessionStart/UserPromptSubmit hooks [to] inject their output into context each session."

## Architecture and Implementation Details

### Proxy Router and Native Hook Runtime

The proxy layer in [`proxy/internal/gateway/server.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/gateway/server.go) handles the initial detection of session boundaries, while [`proxy/internal/nativehook/hook.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/nativehook/hook.go) contains the core `Run` function that manages the lifecycle of the external hook process. This implementation enforces the 250 ms timeout to prevent expensive operations from blocking session creation, and it parses the JSON output to extract the payload for context injection.

### Context Persistence and Storage

After the hook returns successfully, Caveman writes the session context using **atomic writes with restrictive permissions** to prevent malicious project-controlled symlinks from hijacking session state. The `hookSpecificOutput` field is stored atomically on disk in the session store, ensuring that downstream components receive a consistent snapshot of the pre-loaded data even if concurrent modifications occur.

### Downstream Consumption in the Engine

All downstream tool calls and model prompt builders receive the enriched context through the `SessionContext` structure. In [`engine/toon.go`](https://github.com/JuliusBrussee/caveman/blob/main/engine/toon.go), the engine accesses `ctx.HookSpecificOutput` when constructing model requests, allowing prompts to reference data that was pre-loaded during the SessionStart phase. This design eliminates per-turn overhead since the expensive data loading occurs once at session creation rather than during every inference request.

## Implementing a Custom SessionStart Hook

Below is a complete Go implementation that loads a local configuration file and exposes it to the session context:

```go
// File: myhook/session_start.go
package main

import (
	"encoding/json"
	"os"
)

type HookOutput struct {
	HookSpecificOutput map[string]any `json:"hookSpecificOutput"`
}

func main() {
	// Example: load a JSON config file and expose it to the session.
	cfg, _ := os.ReadFile("config.json")
	var data map[string]any
	_ = json.Unmarshal(cfg, &data)

	out := HookOutput{
		HookSpecificOutput: map[string]any{
			"my_config": data,
		},
	}
	enc := json.NewEncoder(os.Stdout)
	_ = enc.Encode(out) // Caveman reads this from stdout.
}

```

Register the hook with the proxy using:

```bash
caveman native-hook myhook SessionStart

```

Downstream code in the engine can then access this data via the session context:

```go
// Inside engine/toon.go (simplified)
func buildPrompt(ctx *SessionContext) string {
	// `ctx.HookSpecificOutput` contains the SessionStart hook output.
	if cfg, ok := ctx.HookSpecificOutput["my_config"]; ok {
		return fmt.Sprintf("Use this config: %v\n%s", cfg, ctx.UserPrompt)
	}
	return ctx.UserPrompt
}

```

## Pi Extension Integration Example

The Pi extension maps its native lifecycle events to Caveman hooks in [`docs/plans/pi-extension.md`](https://github.com/JuliusBrussee/caveman/blob/main/docs/plans/pi-extension.md):

```text
session_start → SessionStart,
before_agent_start → UserPromptSubmit,
…

```

When Pi boots a session, it executes `caveman native-hook pi SessionStart`, passing the JSON payload on stdin. This allows Pi-specific initialization logic to run within Caveman’s hook framework while maintaining compatibility with the generic native-hook transport defined in the proxy runtime.

## Failure Handling and Security Considerations

### Fail-Open Behavior

The SessionStart hook operates on a **fail-open** basis: if the process times out after 250 ms or returns a non-zero exit code, Caveman logs a warning and proceeds with an empty hook payload. Because the hook runs only once per session, any failure does not affect subsequent conversation turns; the session simply continues without the pre-loaded data.

### Atomic Write Protections

To prevent symlink attacks or partial writes, Caveman uses atomic file operations when persisting the session context. The restrictive permissions ensure that only the proxy process can modify the session state file, protecting against malicious attempts to inject data through filesystem manipulation.

## Summary

- The SessionStart hook in Caveman triggers once immediately after session creation, before any user prompt is processed.
- It communicates via stdin/stdout JSON exchange with a strict 250 ms deadline enforced by [`proxy/internal/nativehook/hook.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/nativehook/hook.go).
- The `hookSpecificOutput` field from the hook response is merged into `SessionContext` and persisted atomically in [`proxy/internal/store/learn.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/store/learn.go).
- Downstream components in [`engine/toon.go`](https://github.com/JuliusBrussee/caveman/blob/main/engine/toon.go) access this pre-loaded data when building model requests.
- Failures are handled gracefully with fail-open behavior, while atomic writes prevent session state hijacking.

## Frequently Asked Questions

### What happens if the SessionStart hook exceeds the 250ms deadline?

If the hook process does not complete within 250 milliseconds, the `Run` function in [`proxy/internal/nativehook/hook.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/nativehook/hook.go) terminates the process and logs a timeout warning. Caveman proceeds with session creation using an empty `hookSpecificOutput` payload, ensuring that slow initialization does not block the user experience.

### Can a SessionStart hook access the filesystem or make network requests?

Yes, the SessionStart hook can perform any I/O operation available to its process, including reading files and calling APIs, provided it completes before the deadline. This capability allows the hook to load large configuration files or cache remote resources, though developers should ensure these operations respect the 250 ms timeout to avoid fail-open behavior.

### How does the SessionStart hook differ from the UserPromptSubmit hook?

The SessionStart hook executes exactly once per session during initialization, making it suitable for expensive one-time setup, while the **UserPromptSubmit** hook runs before every user turn. According to the code comments in [`proxy/internal/store/learn.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/store/learn.go), both hooks inject their `hookSpecificOutput` into context, but the SessionStart hook populates the initial session state whereas UserPromptSubmit handles per-turn augmentations.

### Where is the hook output stored and how is it accessed by downstream components?

The hook output is stored in the atomic session context managed by [`proxy/internal/store/learn.go`](https://github.com/JuliusBrussee/caveman/blob/main/proxy/internal/store/learn.go) and accessed through the `SessionContext` structure. Components like [`engine/toon.go`](https://github.com/JuliusBrussee/caveman/blob/main/engine/toon.go) retrieve values from the `HookSpecificOutput` map field when constructing model prompts or executing tool calls, allowing seamless integration of pre-loaded data into the conversation flow.