# Configuration reference

> Environment variables, .env lookup order, reasoning provider priority, web-backend priority, output paths, and trend-monitoring toggles.

- Repository: mvanhorn/last30days-skill
- GitHub: https://github.com/mvanhorn/last30days-skill
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/llms-full.txt

## Source Files

- `CONFIGURATION.md`
- `skills/last30days/scripts/lib/env.py`
- `skills/last30days/scripts/lib/providers.py`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/SKILL.md`

---

---
title: "Configuration reference"
description: "Environment variables, .env lookup order, reasoning provider priority, web-backend priority, output paths, and trend-monitoring toggles."
---

Runtime configuration for `/last30days` resolves through `skills/last30days/scripts/lib/env.py` (`get_config()`), then per-run CLI flags in `last30days.py` override or extend those defaults. The slash-command contract in `skills/last30days/SKILL.md` governs voice and orchestration; this page lists every knob the engine reads.

## Configuration layers

Three layers stack from most transient to most persistent:

| Layer | Mechanism | Typical use |
|---|---|---|
| Per-run flags | `python3 skills/last30days/scripts/last30days.py ...` | `--save-dir`, `--web-backend`, `--store`, `--quick` / `--deep` |
| Process environment | `export VAR=value` before invocation | CI secrets, one-off overrides |
| Files + Keychain | `.claude/last30days.env`, `~/.config/last30days/.env`, macOS Keychain | Long-lived API keys and defaults |

Per-key resolution inside `get_config()` (highest wins):

```mermaid
flowchart TB
  subgraph highest["Highest priority"]
    OS["os.environ"]
  end
  subgraph files["File sources"]
    PROJ[".claude/last30days.env\n(walk up from cwd)"]
    GLOB["~/.config/last30days/.env\nor LAST30DAYS_CONFIG_DIR/.env"]
  end
  subgraph lowest["Lowest priority"]
    KC["macOS Keychain\nlast30days-{KEY}"]
  end
  KC --> MERGE["merged_env"]
  GLOB --> MERGE
  PROJ --> MERGE
  MERGE --> RESOLVE["config[key] = os.environ[key] or merged_env[key]"]
  OS --> RESOLVE
```

<Note>
`LAST30DAYS_CONFIG_DIR=""` disables global file loading (clean/no-config mode). `LAST30DAYS_CONFIG_DIR=/path` relocates the global `.env` to `/path/.env`.
</Note>

<Warning>
POSIX hosts: the engine warns on stderr when a secrets file is group- or world-readable. Set mode `600` on `.env` files.
</Warning>

## `.env` lookup paths

| Path | Scope | Precedence |
|---|---|---|
| `.claude/last30days.env` | Project tree (walk parents from `cwd` until home or root) | Overrides global file |
| `~/.config/last30days/.env` | User default | Fallback when no project file |
| `LAST30DAYS_CONFIG_DIR/.env` | Custom global directory | Replaces default `~/.config/last30days` |

Project-scoped env is the preferred pattern for per-client folders: `cd` into the client directory and run `/last30days` without wrapper scripts.

### macOS Keychain (additive, lowest priority)

On Darwin, `security find-generic-password` loads items with service name `last30days-{KEY}` (for example `last30days-BRAVE_API_KEY`). Keys mirrored in `scripts/setup-keychain.sh` include all major API credentials in `env.KEYCHAIN_KEYS`. Keychain never overrides values already present in `.env` or `os.environ`.

## Output paths

### `LAST30DAYS_MEMORY_DIR`

| Platform | Default when unset |
|---|---|
| Linux / macOS | `~/Documents/Last30Days/` |
| Windows | `C:\Users\<you>\Documents\Last30Days\` |

The skill passes `--save-dir="${LAST30DAYS_MEMORY_DIR}"` on agent invocations. Override with the env var or per-run `--save-dir <path>`.

### Filename rules

Each saved run uses a slug derived from the topic:

| Pattern | When |
|---|---|
| `<slug>-raw[-suffix].md` | Default markdown artifact |
| `<slug>-raw-html[-suffix].html` | `--emit=html` |
| `<slug>-raw[-suffix].json` | `--emit=json` |

- **`--save-suffix <name>`** — isolates variants (for example per client: `--save-suffix=acme`).
- **Collision** — same slug + suffix on the same calendar day overwrites; on a different day the engine appends `-YYYY-MM-DD` before the extension.

The footer line `📎 Raw results saved to …/<slug>-raw.md` is the canonical pointer in synthesized output.

### Trend-monitoring storage (separate from markdown saves)

| Artifact | Default path |
|---|---|
| SQLite research store | `~/.local/share/last30days/research.db` |
| Briefing JSON snapshots | `~/.local/share/last30days/briefs/` |
| Last-run metadata | `{LAST30DAYS_CONFIG_DIR or ~/.config/last30days}/last-run.json` |

Markdown under `LAST30DAYS_MEMORY_DIR` and SQLite under `~/.local/share/last30days/` are independent; `--store` / `LAST30DAYS_STORE` only affects SQLite.

## Reasoning provider priority

Headless runs (cron, watchlist, direct CLI without a hosting model supplying `--plan`) need one provider for planner + rerank. Pin explicitly or rely on auto-detect.

<ParamField body="LAST30DAYS_REASONING_PROVIDER" type="string" default="auto">
`auto` | `gemini` | `openai` | `xai` | `openrouter`. Resolved in `lib/providers.py` → `resolve_runtime()`.
</ParamField>

**Auto-detect order** (first match wins):

| Priority | Provider | Credential signal |
|---|---|---|
| 1 | Gemini | `GOOGLE_API_KEY`, `GEMINI_API_KEY`, or `GOOGLE_GENAI_API_KEY` |
| 2 | OpenAI | `OPENAI_API_KEY` with `OPENAI_AUTH_STATUS=ok` |
| 3 | xAI | `XAI_API_KEY` |
| 4 | OpenRouter | `OPENROUTER_API_KEY` |
| 5 | Local | No keys — deterministic planner, `local-score` rerank |

<Info>
When `/last30days` runs inside Claude Code, Codex, Gemini CLI, or similar, the **host model** is the reasoning provider for planning and synthesis. API keys above matter for headless `last30days.py` runs and for engine-side rerank when the host does not pass `--plan`.
</Info>

### Model pins

| Variable | Role |
|---|---|
| `LAST30DAYS_PLANNER_MODEL` | Override planner model for the resolved provider |
| `LAST30DAYS_RERANK_MODEL` | Override rerank model; Gemini `deep` profile defaults rerank to `gemini-3.1-pro-preview` |
| `OPENAI_MODEL_PIN` | Legacy pin alias consumed alongside provider defaults |
| `XAI_MODEL_PIN` | Legacy pin alias for X search and xAI defaults |
| `LAST30DAYS_X_MODEL` | X search model when using xAI backend |

Provider defaults (when pins unset): Gemini `gemini-3.1-flash-lite`, OpenAI `gpt-5.4-nano`, xAI `grok-4-1-fast`, OpenRouter `google/gemini-3.1-flash-lite-preview`. Gemini planner/rerank models must use the `gemini-3.1-*` prefix or the engine raises at startup.

### X search backend pin

<ParamField body="LAST30DAYS_X_BACKEND" type="string">
Optional pin: `xai` or `bird`. When unset, resolution order is `XAI_API_KEY` → explicit `AUTH_TOKEN` + `CT0` (Bird) → `xurl` CLI (OAuth2).
</ParamField>

## Web search backend priority

Grounding supplements (`grounding` source) power `--auto-resolve` and Step 2 web supplements when the host has no native WebSearch.

<ParamField body="--web-backend" type="choice" default="auto">
CLI choices: `auto`, `brave`, `exa`, `serper`, `parallel`, `none`. Implemented in `lib/grounding.py` → `web_search()`.
</ParamField>

**Auto-detect order** (in `grounding.web_search` and `pipeline.diagnose`):

| Priority | Backend | Key |
|---|---|---|
| 1 | Brave | `BRAVE_API_KEY` |
| 2 | Exa | `EXA_API_KEY` |
| 3 | Serper | `SERPER_API_KEY` |
| 4 | Parallel | `PARALLEL_API_KEY` |
| 5 | None / host | Empty key set — rely on harness native WebSearch (Claude Code, Codex, Gemini) |

`--web-backend=none` drops `grounding` from the active source list. Pinning `brave` without `BRAVE_API_KEY` raises a runtime error.

<Warning>
Thin cross-host results often trace to missing web keys on headless runs. Run `--diagnose` and compare `native_web_backend` against a machine that has Brave (or another key) configured.
</Warning>

## Source credentials and toggles

### API keys by source

| Source | Key(s) | Notes |
|---|---|---|
| Reddit, HN, Polymarket | none | Always on |
| GitHub | `gh` CLI auth or `GITHUB_TOKEN` | CLI preferred |
| YouTube | `yt-dlp` on PATH; optional `LAST30DAYS_YOUTUBE_SSH_HOST` | SSH-routes yt-dlp for datacenter bot-wall |
| X / Twitter | `XAI_API_KEY` **or** `AUTH_TOKEN`+`CT0` **or** `FROM_BROWSER` **or** xurl OAuth | See X backend table above |
| TikTok / Instagram / Threads | `SCRAPECREATORS_API_KEY` | Comma-separated keys rotate per run; legacy alias `SCRAPE_CREATORS_API_KEY` accepted |
| Pinterest | `SCRAPECREATORS_API_KEY` + request `pinterest` via `--search` | Opt-in at query time |
| Bluesky | `BSKY_HANDLE`, `BSKY_APP_PASSWORD` | App password `xxxx-xxxx-xxxx-xxxx` |
| Bluesky host | `BSKY_SEARCH_HOST` | Default `api.bsky.app` |
| Truth Social | `TRUTHSOCIAL_TOKEN` | Bearer from browser session |
| Web grounding | `BRAVE_API_KEY` / `EXA_API_KEY` / `SERPER_API_KEY` / `PARALLEL_API_KEY` | One is enough for auto mode |
| Perplexity Sonar | `OPENROUTER_API_KEY` + `INCLUDE_SOURCES=perplexity` | Additive source |
| Deep Research | `OPENROUTER_API_KEY` + `--deep-research` | ~$0.90/query |
| Apify (legacy) | `APIFY_API_TOKEN` | Fallback when ScrapeCreators exhausted |
| Xiaohongshu | `XIAOHONGSHU_API_BASE` | Default `http://host.docker.internal:18060` |
| Xquik | `XQUIK_API_KEY` | Alternate X search |

### `INCLUDE_SOURCES` and `EXCLUDE_SOURCES`

Comma-separated, case-insensitive lists in `.env` or environment.

| Variable | Behavior |
|---|---|
| `INCLUDE_SOURCES` | **Required** for `perplexity`, `youtube_comments`, `tiktok_comments`. When non-empty, quality scoring treats sources not listed as intentionally disabled. |
| `EXCLUDE_SOURCES` | Removes matching sources from `pipeline.available_sources()` (for example `EXCLUDE_SOURCES=threads,pinterest`). |

ScrapeCreators-backed TikTok, Instagram, and Threads enter the available set when `SCRAPECREATORS_API_KEY` is set; use `EXCLUDE_SOURCES` or `--search` to narrow the fan-out.

### Browser cookie extraction

<ParamField body="FROM_BROWSER" type="string">
`off` — disable extraction. `firefox` / `chrome` / `safari` — single browser. `auto` — try Firefox, Safari, then Chrome (Chrome may trigger Keychain prompts on macOS). **Default (unset):** Firefox and Safari only (silent).
</ParamField>

Extracted cookies map to `AUTH_TOKEN`/`CT0` (X) and `TRUTHSOCIAL_TOKEN` without overwriting explicit env values.

### Other engine tunables

| Variable | Default | Purpose |
|---|---|---|
| `LAST30DAYS_TRANSCRIPT_TIMEOUT` | `30` (seconds) | Instagram/SC transcript fetch timeout |
| `LAST30DAYS_DEBUG` | off | HTTP debug logging to stderr (`1` / `true` / `yes`) |
| `LAST30DAYS_SKIP_PREFLIGHT` | off | Bypass Class-1 keyword-trap refuse gate (exit 2) |
| `SETUP_COMPLETE` | — | Set by setup wizard after first configuration |
| `CODEX_AUTH_FILE` | `~/.codex/auth.json` | Codex auth path (OpenAI via API key is preferred; Codex JWT path is not used for planner auth in current `get_openai_auth`) |

## Trend monitoring toggles

Snapshot mode (default) writes slug-named markdown only. Continuous monitoring adds SQLite persistence plus optional scheduling scripts.

### Persistence toggle

| Mechanism | Effect |
|---|---|
| `--store` | Persist ranked findings to SQLite after the run |
| `LAST30DAYS_STORE=1` (or `true` / `yes`) | Same as `--store` on every run; flag and env are additive |

Findings dedupe on `source_url` (UNIQUE). Tables: `topics`, `research_runs`, `findings`, `settings` (`scripts/store.py`).

### Watchlist and briefings

| Script | Role |
|---|---|
| `scripts/watchlist.py` | `add`, `remove`, `list`, `run-one`, `run-all`, `config` — schedule metadata + Slack/webhook delivery on new findings |
| `scripts/briefing.py` | `generate`, `generate --weekly`, `show [--date DATE]` — structured digest data under `briefs/` |

Watchlist spawns the engine with `--quick` and `--lookback-days 90`. The stored `schedule` field is metadata; **cron / Task Scheduler / GitHub Actions** must call `run-one` or `run-all`.

<Steps>
<Step title="Baseline snapshot">
Run once with persistence: `/last30days "<topic>" --days=30 --store` (or `LAST30DAYS_STORE=1` in `.env`).
</Step>
<Step title="Register topic">
`python3 skills/last30days/scripts/watchlist.py add "<topic>" --weekly`
</Step>
<Step title="Schedule runs">
External scheduler: `python3 skills/last30days/scripts/watchlist.py run-all`
</Step>
<Step title="Weekly digest">
`python3 skills/last30days/scripts/briefing.py generate --weekly`
</Step>
</Steps>

## Verification

```bash
python3 skills/last30days/scripts/last30days.py --diagnose
```

Emits JSON with `providers`, `reasoning_provider`, `x_backend`, `native_web_backend`, `has_scrapecreators`, `has_github`, and `available_sources` — no full search executed.

<Check>
After editing `.env`, re-run `--diagnose` before a production topic. Confirm `native_web_backend` and expected sources appear under `available_sources`.
</Check>

## Example `.env` skeleton

```bash
# Reasoning (one provider; auto picks first match)
GOOGLE_API_KEY=<gemini-key>

# Web grounding (one key is enough)
BRAVE_API_KEY=<brave-key>

# Optional ScrapeCreators family
SCRAPECREATORS_API_KEY=<sc-key>
INCLUDE_SOURCES=tiktok,instagram,threads

# Always-on persistence
LAST30DAYS_STORE=1

# Output location
LAST30DAYS_MEMORY_DIR=$HOME/Clients/acme/Research/Last30Days
```

```bash
chmod 600 ~/.config/last30days/.env
# or: chmod 600 .claude/last30days.env
```

## Related pages

<CardGroup>
<Card title="Configure sources" href="/configure-sources">
Credential layers, setup wizard, and per-source availability checks.
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Project-scoped `.claude/last30days.env`, save-dir isolation, and client wrappers.
</Card>
<Card title="Trend monitoring" href="/trend-monitoring">
SQLite schema, watchlist delivery, briefing synthesis, and cadence patterns.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `last30days.py` flag surface including `--emit` and targeting flags.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`--diagnose` recovery, thin results, and stale marketplace installs.
</Card>
</CardGroup>
