# Per-client setup

> Project-scoped .claude/last30days.env, save-dir and --save-suffix isolation, category peer subreddits, and recurring --competitors-plan templates.

- 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/categories.py`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`

---

---
title: "Per-client setup"
description: "Project-scoped .claude/last30days.env, save-dir and --save-suffix isolation, category peer subreddits, and recurring --competitors-plan templates."
---

Per-client isolation in last30days-skill is configuration-driven: a project-scoped `.claude/last30days.env` overrides global credentials, `LAST30DAYS_MEMORY_DIR` and `--save-suffix` partition saved research files, `scripts/lib/categories.py` supplies vertical-specific Reddit peers, and checked-in `--competitors-plan` JSON templates reuse Step 0.55 targeting for recurring comparison runs.

## Configuration layers

```text
  Process environment (highest)
           │
           ▼
  .claude/last30days.env   ← walk-up from cwd (stops at $HOME)
           │
           ▼
  ~/.config/last30days/.env   ← global default
           │
           ▼
  macOS Keychain (last30days-* prefix, Darwin only)
```

<ParamField body="LAST30DAYS_CONFIG_DIR" type="string">
Override the global config directory. Set to an empty string for no-config (clean) mode. When unset, global config lives at `~/.config/last30days/.env`.
</ParamField>

The engine loads API keys and source toggles from this stack via `get_config()` in `scripts/lib/env.py`. Project env discovery walks parent directories from `Path.cwd()` until it finds `.claude/last30days.env` or reaches `$HOME`.

<Warning>
POSIX secret files should be mode `600`. The engine warns on every run when group/other read bits are set; Claude Code’s `check-config.sh` hook applies the same check at session start.
</Warning>

## Pattern 1: Project-scoped `.claude/last30days.env`

Place one env file per client workspace:

```text
acme-corp/
  .claude/
    last30days.env
  ...
```

Typical contents isolate credentials, enabled sources, and the save root:

```bash
LAST30DAYS_MEMORY_DIR=/Users/you/Clients/acme/Research/Last30Days
SCRAPECREATORS_API_KEY=<client-or-shared-key>
INCLUDE_SOURCES=tiktok,instagram
BSKY_HANDLE=acme.bsky.social
BSKY_APP_PASSWORD=<app-password>
```

<Steps>
<Step title="Create the file">
Add `.claude/last30days.env` at the client repo root (or any ancestor of where you run research). Restrict permissions: `chmod 600 .claude/last30days.env`.
</Step>
<Step title="Work from the client directory">
`cd` into the client folder before `/last30days <topic>`. The engine resolves the nearest project env on each run; no wrapper script is required for the common case.
</Step>
<Step title="Verify keys loaded">
Run `python3 skills/last30days/scripts/last30days.py --diagnose` from the client directory. Confirm per-source availability matches the client’s `.env` contents.
</Step>
</Steps>

<Note>
Slash-command invocations pass `--save-dir="${LAST30DAYS_MEMORY_DIR}"` from the SKILL.md bash preamble. Export `LAST30DAYS_MEMORY_DIR` in the shell (or have the hosting model read it from `.claude/last30days.env` before the engine call) so saves land in the client folder. API keys in the project file are picked up by `get_config()` without extra export.
</Note>

## Pattern 2: Save-dir and suffix isolation

Saved artifacts use slug-based filenames under the active save directory.

| Mechanism | Scope | Effect |
|-----------|-------|--------|
| `LAST30DAYS_MEMORY_DIR` | Persistent per client | Root directory for all saves when passed as `--save-dir` |
| `--save-dir <path>` | Single run | One-off output location |
| `--save-suffix <name>` | Single run | Inserts `-<suffix>` before the extension |

**Filename rules** (`save_output()` in `scripts/last30days.py`):

- Default markdown: `<slug>-raw[-suffix].md`
- JSON: `<slug>-raw[-suffix].json`
- HTML: `<slug>-raw-html[-suffix].html`
- Same topic + suffix on the same calendar day overwrites; a collision on a later day appends `-YYYY-MM-DD` before the extension.

Default save root when unset: `~/Documents/Last30Days/` (Windows: `C:\Users\<you>\Documents\Last30Days\`).

### Wrapper for batch or multi-client hosts

When you do not `cd` into each client tree, set the memory dir and suffix in one shell function:

<CodeGroup>
```bash title="Bash"
l30d-client() {
  local client=$1; shift
  LAST30DAYS_MEMORY_DIR="$HOME/Clients/$client/Research/Last30Days" \
    python3 skills/last30days/scripts/last30days.py "$@" \
      --emit=compact \
      --save-dir="$LAST30DAYS_MEMORY_DIR" \
      --save-suffix="$client"
}
# l30d-client acme "british airways middle east"
```

```powershell title="PowerShell"
function Run-L30D-Client {
    param([string]$ClientSlug, [Parameter(ValueFromRemainingArguments=$true)]$Args)
    $env:LAST30DAYS_MEMORY_DIR = "C:\Users\$env:USERNAME\Clients\$ClientSlug\Research\Last30Days"
    python3 skills/last30days/scripts/last30days.py @Args `
      --emit=compact `
      --save-dir=$env:LAST30DAYS_MEMORY_DIR `
      --save-suffix=$ClientSlug
}
# Run-L30D-Client acme "british airways middle east"
```
</CodeGroup>

<Info>
Combine Pattern 1 and Pattern 2: project env sets `LAST30DAYS_MEMORY_DIR` for the client tree; add `--save-suffix=<client-slug>` when multiple clients share one save root and you need distinct filenames for the same topic slug.
</Info>

### Comparison mode saves

Vs-mode (`--competitors-plan` or `--competitors`) writes one file per entity plus merged stdout output. Peer entities save as `{peer-slug}-raw[-suffix].md`; the main topic uses the leading entity slug. All paths honor the same `--save-dir` and `--save-suffix`.

## Pattern 3: Category peer subreddits

Brand-specific subreddits from WebSearch miss cross-product technique threads (the documented `GPT Image 2` failure mode). Step 0.55 Section 2a requires appending category peers for product topics.

**Hosting-model path (Claude Code, Codex, Gemini, etc.):** SKILL.md carries the canonical category table and merging rule—start with WebSearch subs, append 2–3 peers in priority order, dedupe case-insensitively, cap at 10 (WebSearch subs win when trimming). Emit `(+ {category_id} peers)` on the Resolved Reddit line when peers were added.

**Engine `--auto-resolve` path:** `scripts/lib/resolve.py` calls `categories.detect_category()` and `categories.peer_subs_for()` with the same cap and preservation rules.

### Extending the map

`scripts/lib/categories.py` defines `CATEGORY_PEERS`: each entry has `patterns` (compound terms, first-match-wins in declaration order) and `peer_subs` (priority-ordered subreddit names without `r/` prefix).

```python
# Illustrative new row — add inside CATEGORY_PEERS in categories.py
"legal_tech": {
    "patterns": ["legal tech", "contract automation", "clm platform"],
    "peer_subs": ["legaltech", "LawFirm", "SaaS"],
},
```

<Warning>
There is no runtime override file for categories. New verticals require a code change (or a beta/private fork). Keep patterns specific—bare tokens like `ai` or `model` are intentionally avoided to limit false positives.
</Warning>

| Category id | Example triggers | Peer subs (first entries) |
|-------------|------------------|---------------------------|
| `ai_image_generation` | gpt image, midjourney, stable diffusion | StableDiffusion, midjourney, dalle2, aiArt |
| `ai_coding_agent` | claude code, cursor ide, openclaw | ChatGPTCoding, LocalLLaMA, singularity |
| `saas_screen_recording` | loom video, tella screen | SaaS, screenrecording, productivity |
| `prediction_markets` | polymarket, kalshi | Polymarket, Kalshi, predictionmarkets |

Person topics, news stories, and topics outside any category skip peer expansion.

## Pattern 4: Recurring `--competitors-plan` templates

Store per-client competitor targeting as JSON beside the client workspace. Reuse across scheduled or ad hoc comparison runs instead of re-running full Step 0.55 discovery every time.

### Schema

Top-level object: entity name → targeting dict. Keys are normalized to lowercase. Inline JSON or a file path is accepted (same behavior as `--plan`).

<ParamField body="x_handle" type="string">
Official X/Twitter handle for the entity (`@` optional).
</ParamField>

<ParamField body="x_related" type="string[]">
Related handles searched at lower weight.
</ParamField>

<ParamField body="subreddits" type="string[]">
Subreddit names without `r/` prefix.
</ParamField>

<ParamField body="github_user" type="string">
GitHub username for person-mode search.
</ParamField>

<ParamField body="github_repos" type="string[]">
`owner/repo` strings for project-mode search.
</ParamField>

<ParamField body="context" type="string">
Free-text news/founding context passed into sub-runs.
</ParamField>

Unknown fields log a stderr warning and are ignored. Malformed JSON or a non-object top level exits with code 2.

### Example template

```json
{
  "Anthropic": {
    "x_handle": "AnthropicAI",
    "subreddits": ["ClaudeAI", "LocalLLaMA", "artificial"],
    "github_user": "anthropics",
    "context": "Founded 2021, Claude family of models"
  },
  "xAI": {
    "x_handle": "xai",
    "subreddits": ["singularity", "LocalLLaMA"],
    "github_user": "xai-org",
    "context": "Grok models, X integration"
  }
}
```

### Invocation

Main topic (first in the `vs` string) uses outer flags (`--x-handle`, `--subreddits`, `--github-user`, …). Peers take targeting from the plan via `subrun_kwargs_for()`—plan values beat engine `auto_resolve`, and main-topic flags cannot leak into peer sub-runs.

<RequestExample>
```bash
# Write JSON to a tmpfile when context strings contain apostrophes (SKILL.md heredoc pattern)
COMPETITORS_PLAN_FILE=$(mktemp "${TMPDIR:-/tmp}/last30days-competitors.XXXXXX")
trap 'rm -f "$COMPETITORS_PLAN_FILE"' EXIT
cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF'
{"Anthropic":{"x_handle":"AnthropicAI","subreddits":["ClaudeAI","LocalLLaMA"]}}
PLAN_EOF

python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic vs xAI" \
  --emit=compact \
  --save-dir="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}" \
  --save-suffix=acme \
  --x-handle=OpenAI \
  --subreddits=OpenAI,ChatGPT,singularity \
  --competitors-plan "$COMPETITORS_PLAN_FILE"
```
</RequestExample>

<Check>
Prefer `--competitors-plan` over `--competitors-list` when peers need handles and subreddits. List-only mode falls back to thin planner defaults; empty fields in the Resolved Entities block indicate skipped Step 0.55 for that entity.
</Check>

File-path form for checked-in templates:

```bash
--competitors-plan clients/acme/competitors-plan.json
```

Refresh templates when handles, primary repos, or category peers change—peer sub-runs do not inherit the main topic’s subreddit list.

## Composing patterns

```text
┌─────────────────────────────────────────────────────────────┐
│ Client workspace                                            │
│  .claude/last30days.env  → keys, INCLUDE_SOURCES, save root │
│  competitors-plan.json   → recurring vs-mode targeting      │
│  (optional) categories.py fork row → vertical peer subs     │
└───────────────────────────┬─────────────────────────────────┘
                            │
         cd + /last30days    │    wrapper: LAST30DAYS_MEMORY_DIR
                            │              + --save-suffix
                            ▼
              Engine: get_config() + --save-dir + plan JSON
                            │
                            ▼
              ~/Clients/<slug>/Research/Last30Days/
                <topic>-raw-<suffix>.md
                <peer>-raw-<suffix>.md   (comparison)
```

| Client need | Primary pattern | Secondary |
|-------------|-----------------|-----------|
| Separate API keys per account | `.claude/last30days.env` | — |
| Separate output folders | `LAST30DAYS_MEMORY_DIR` in project env | `--save-dir` override |
| Same folder, same topic, different clients | `--save-suffix` | wrapper export |
| Niche SaaS vertical | `categories.py` row | SKILL.md 2a for model path |
| Monthly competitor brief | `competitors-plan.json` | `--competitors` for discovery refresh |

## Beta and private forks

Client-specific category rows, internal subreddit lists, and industry plan templates that should not ship on the public marketplace belong on the beta/private companion install (`/last30days-beta` from `mvanhorn/last30days-skill-private`). Promote upstream only through review against the public repo.

<Tip>
After editing `categories.py` or plan templates in a dev checkout, re-run `npx skills add . -g -y` or symlink `~/.agents/skills/last30days` to the working tree so harnesses load the updated skill copy.
</Tip>

## Verification

| Check | Command / signal |
|-------|------------------|
| Project env found | `--diagnose` shows expected keys; `_CONFIG_SOURCE` in debug paths may read `project:.claude/last30days.env` |
| Saves isolated | Footer line `📎 Raw results saved to …/<slug>-raw-<suffix>.md` points under the client `LAST30DAYS_MEMORY_DIR` |
| Category peers applied | Resolved block includes `(+ ai_image_generation peers)` or engine log `[Resolve] Matched category=…` |
| Plan threaded | No dashes in per-entity Resolved block; peer raw files exist for each competitor |

## Related pages

<CardGroup>
<Card title="Configure sources" href="/configure-sources">
Credential layers, `INCLUDE_SOURCES`, setup wizard, and `--diagnose` availability checks.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full environment variable and `.env` lookup order.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
`--competitors` fan-out, plan schema details, and synthesis scaffold.
</Card>
<Card title="Comparison recipes" href="/comparison-recipes">
Copy-paste vs-topic workflows and Head-to-Head table expectations.
</Card>
<Card title="Model clients" href="/model-clients">
Per-harness install patterns and stale-install avoidance.
</Card>
<Card title="Beta channel" href="/beta-channel">
Private-repo customizations that stay off the public marketplace.
</Card>
</CardGroup>
