# How gstack Domain Skills Persist Per-Site Knowledge Across Sessions

> Learn how gstack domain skills persist per-site knowledge across sessions. Discover the JSONL append-only record system and state management that ensures continuity.

- Repository: [Garry Tan/gstack](https://github.com/garrytan/gstack)
- Tags: how-to-guide
- Published: 2026-05-15

---

**gstack domain skills persist per-site knowledge across sessions by writing append-only JSONL records to project-scoped or global files in `~/.gstack`, using host normalization and a state machine to manage quarantined, active, and global states.**

The gstack browser agent (garrytan/gstack) implements a lightweight knowledge management system called **domain skills** that captures per-site insights and makes them available across process restarts. Unlike ephemeral context windows, this system ensures that lessons learned about specific websites survive session termination through durable, append-only storage and a careful promotion lifecycle.

## Storage Architecture and File Locations

Domain skills use two distinct storage scopes depending on visibility requirements. According to the source code in [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts) (lines 69-75), the system defines the following paths:

- **Project-scoped notes**: `~/.gstack/projects/<project-slug>/learnings.jsonl`
- **Global notes**: `~/.gstack/global-domain-skills.jsonl`

The helper functions that resolve these paths are implemented as follows:

```typescript
// storage paths
function globalFile(): string { 
  return path.join(gstackHome(), 'global-domain-skills.jsonl'); 
}

function projectFile(slug: string): string { 
  return path.join(gstackHome(), 'projects', slug, 'learnings.jsonl'); 
}

```

## Host Normalization for Consistent Keys

Before any write operation, the system canonicalizes hostnames to ensure that `https://www.Example.com:443` and `example.com` resolve to the same key. The `normalizeHost` function (lines 79-90 in [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts)) strips protocols, ports, and the `www.` prefix, then lowercases the result.

```typescript
export function normalizeHost(input: string): string { … }

```

This normalization guarantees that every visit to the same origin maps to a single, persistent knowledge entry regardless of URL formatting variations.

## The Save Operation and Data Structure

When an agent generates a note, the `writeSkill` function creates a **quarantined** row and appends it to the project-level JSONL file. As implemented in [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts) (lines 52-85), each row contains:

- The full markdown body
- A SHA-256 hash for integrity
- Timestamps and version numbers
- The classifier score (must be < 0.85 to proceed)

```typescript
export async function writeSkill(input: WriteSkillInput): Promise<DomainSkillRow> {
    // …compute hash, version, create row…
    await appendRow(projectFile(input.projectSlug), row);
}

```

## State Machine Lifecycle

The domain skill system employs a three-state machine to prevent prompt injection attacks and ensure quality control, documented in the header comment (lines 9-22) and enforced throughout [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts).

### Quarantined State

All newly saved notes begin in the **quarantined** state. These rows are persisted to disk but are explicitly excluded from prompts. This isolation prevents untested knowledge from immediately influencing agent behavior.

### Promotion to Active

After a quarantined skill has been used `PROMOTE_THRESHOLD` (3) times without triggering classifier flags, the `recordSkillUse` function (lines 18-26) automatically promotes it to **active**. Only active skills are eligible for injection into agent prompts.

### Global Promotion

An explicit `domain-skill promote-to-global` command invokes `promoteToGlobal` (lines 42-71), which copies the active row to the global file. This makes the note visible to every project, effectively creating a cross-project knowledge base.

## Reading and Retrieval Logic

When the browser agent needs context for a specific site, the `readSkill` function (lines 20-38) executes a hierarchical lookup:

1. First, search the project-specific `learnings.jsonl` file
2. If not found, fall back to `~/.gstack/global-domain-skills.jsonl`
3. Return only rows whose state is `active` (project) or `global`

Quarantined rows are filtered out during this retrieval phase, ensuring that unverified content never reaches the context window.

```typescript
export async function readSkill(host: string, projectSlug: string): Promise<ReadSkillResult | null> {
    // look at project → global, ignore quarantined rows
}

```

## Durability and Crash Safety

All writes use `fs.open` with the `O_APPEND` flag followed by an explicit `fsync` call. This guarantees that each JSON Lines entry is atomically persisted (under Linux PIPE_BUF limits) and survives process crashes. Deletions are handled via tombstone rows rather than file rewrites, with a periodic compactor routine rewriting files to reclaim space.

## Command-Line Interface

The top-level dispatcher in [`browse/src/domain-skill-commands.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skill-commands.ts) (lines 58-99) exposes the domain skill system through the `$B domain-skill` sub-command structure:

```bash

# Save the current page’s note (agent writes the body to stdin)

$ B domain-skill save < notes.md

# List all active notes for the current project (project + global)

$ B domain-skill list

# Show the full markdown body for a specific host

$ B domain-skill show example.com

# Promote a project-local note to the global pool

$ B domain-skill promote-to-global example.com

# Roll back a note to the previous version

$ B domain-skill rollback example.com --global

```

Programmatic usage is also supported by importing the core functions:

```typescript
import { writeSkill, readSkill, recordSkillUse } from './domain-skills';

// Save a new note (quarantined)
await writeSkill({
  host: 'example.com',
  body: '# My notes\n…',

  projectSlug: 'my-project',
  source: 'agent',
  classifierScore: 0.12, // must be < 0.85
});

// When the agent later uses the note:
await recordSkillUse('example.com', 'my-project', false);

// Retrieve the note for prompting:
const skill = await readSkill('example.com', 'my-project');
if (skill) {
  // skill.row.body contains the markdown
}

```

## Summary

- **gstack domain skills** store per-site knowledge in append-only JSONL files at `~/.gstack/projects/<slug>/learnings.jsonl` or `~/.gstack/global-domain-skills.jsonl`
- **Host normalization** ensures consistent keys across protocol variations via `normalizeHost()` in [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts)
- **Three-state lifecycle** (quarantined → active → global) protects against prompt injection through classifier gating and usage thresholds
- **Atomic durability** is achieved through `O_APPEND` writes with explicit `fsync` calls
- **Hierarchical retrieval** checks project scope first, then global, ignoring quarantined entries

## Frequently Asked Questions

### Where does gstack store domain skills?

gstack writes project-scoped domain skills to `~/.gstack/projects/<project-slug>/learnings.jsonl` and global skills to `~/.gstack/global-domain-skills.jsonl`, as defined by the `projectFile()` and `globalFile()` functions in [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts) (lines 69-75).

### How does gstack prevent malicious content in domain skills?

The system implements a **quarantine** state for all new notes. Content remains isolated from prompts until `recordSkillUse()` promotes it after `PROMOTE_THRESHOLD` (3) successful uses without classifier flags. Additionally, the `writeSkill` function requires a classifier score below 0.85 before accepting new entries.

### What triggers a domain skill to become active?

A domain skill transitions from **quarantined** to **active** automatically after being successfully referenced three times without triggering safety classifiers. This promotion logic resides in `recordSkillUse()` within [`browse/src/domain-skills.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/domain-skills.ts) (lines 18-26).

### Can domain skills be shared across projects?

Yes. Once a skill reaches the **active** state within a project, you can promote it to global scope using `B domain-skill promote-to-global <host>`. This copies the row to `~/.gstack/global-domain-skills.jsonl`, making it available to all projects when `readSkill()` performs its fallback lookup.