---
version: v0.36.5.0
date: 2026-05-18
feature_pitch: >
  Shell jobs grow `inherit: [...]` — agent passes any snake_case config-key
  names; worker resolves values from its own `loadConfig()` at child-spawn.
  Names persist to the row; values resolve fresh. Validated pre-enqueue.
---

# v0.36.5.0 — Free-form secret inheritance for shell jobs calling `gbrain` CLI

## What changed

- **Shell-job pre-enqueue validator** (`src/core/minions/handlers/shell-validate.ts`,
  NEW). Called from BOTH submit surfaces BEFORE `MinionQueue.add()`:
  `gbrain jobs submit shell` (CLI) and `submit_job` MCP op for `name='shell'`.
  Validates cmd/argv/cwd/env shape, `inherit` array of snake_case names, and
  fail-fasts if the worker can't resolve a requested name. No rejected
  payload ever lands in `minion_jobs.data`.
- **`inherit: [...]` ShellJobParams field — free-form.** Pass any snake_case
  config-key name (`database_url`, `anthropic_api_key`, `voyage_api_key`,
  `groq_api_key`, `zeroentropy_api_key`, `my_custom_field`, etc.). The
  validator does NOT police which keys you choose — same-uid trust model
  treats the agent as a peer.
- **Env-key derivation**: name uppercased by default. One override:
  `database_url` → `GBRAIN_DATABASE_URL` (plain `DATABASE_URL` is ambiguous
  in most Postgres-app contexts).
- **Prototype-pollution defense**: snake_case regex blocks `__proto__` /
  `_leading` / uppercase. Value-resolver uses `Object.hasOwn`.
- **Defense-in-depth re-validation** in the shell handler at job-pickup
  catches pre-existing rows AND any future submit path that forgets the
  pre-enqueue call.
- **`gbrain doctor home_dir_in_worktree` check** warns when `~/.gbrain/`
  lives inside a git worktree. Handles `.git` as directory or file.
- **`~/.gbrain/.gitignore` retroactive** via `ensureGitignore()` called from
  `saveConfig()` AND `gbrain post-upgrade`. Idempotent, never clobbers.
- **Output-side redaction (opt-in)**: set `redact_secrets: true` on the job
  params (or pass `--redact-secrets` on the CLI) and the worker scrubs every
  occurrence of resolved `inherit:` values from `stdout_tail` /
  `stderr_tail` / `error_text` before persistence. Replacement token:
  `<REDACTED:name>`. Literal-string replace — defeats accidental
  `echo "$GBRAIN_DATABASE_URL"`, not adversarial encode-then-print. Default
  false (back-compat).
- New canonical guide: `docs/guides/agent-to-gbrain.md`. Two-domain framing
  for downstream agent authors: MCP ops via thin-client OAuth vs `localOnly`
  admin ops via shell-job `inherit:`.

## What downstream agents can do

The agent picks which secrets to inherit, per job, by name:

```jsonc
{
  "cmd": "gbrain sync --skip-failed && gbrain embed --stale",
  "cwd": "/data/gbrain",
  "inherit": ["database_url", "anthropic_api_key", "voyage_api_key"]
}
```

Worker resolves each name from `loadConfig()` and injects into the child env
under the derived key (`database_url` → `GBRAIN_DATABASE_URL`,
`anthropic_api_key` → `ANTHROPIC_API_KEY`, etc.). Names land in
`minion_jobs.data` and the shell-audit JSONL; values resolve fresh on every
spawn and don't persist from `inherit:` itself.

If the worker can't resolve a name, the validator fail-fasts at submit time
with a paste-ready `gbrain config set <name> <value>` hint.

You can still use `env:` for non-secret values or for secrets you've decided
are OK to persist in the row (e.g. correlation tokens). v0.36.5.0 doesn't
forbid that — the validator trusts the agent.

## Verify

```bash
# 1. The new pattern works on your worker:
gbrain jobs submit shell --params \
  '{"cmd":"gbrain stats","cwd":"/tmp","inherit":["database_url"]}' --follow
# Expect: page count, exit 0.

# 2. The doctor surfaces worktree risk if it exists:
gbrain doctor --json | grep -A1 home_dir_in_worktree

# 3. The retroactive gitignore landed:
test -f ~/.gbrain/.gitignore && cat ~/.gbrain/.gitignore
# Expect: file exists, contents = "*\n"

# 4. The audit-log records names not values:
tail -1 ~/.gbrain/audit/shell-jobs-*.jsonl | grep -o '"inherit":\[[^]]*\]'
# Expect: ["database_url"]  (no URL value)
```

## Why this exists

PR #1137 documented two workarounds for the env-stripping behavior of shell
jobs: write `database_url` plaintext to `~/.gbrain/config.json`, or pass
`env: { GBRAIN_DATABASE_URL: ... }` per-job. Both work. Both leave plaintext
secrets either on disk or in `minion_jobs.data` rows that travel with brain
DB dumps and shared brains.

The `/cso` audit cut an earlier proposal (encrypted vault + Unix-socket
broker + SO_PEERCRED + per-call tokens) as theater for a single-uid topology.
Codex's pre-landing review then caught the load-bearing bug in the rewritten
"minimum scope" plan: validation in the handler runs AFTER `queue.add()`,
so the headline "input-secrets never persist" was technically false. Fixed
pre-implementation by lifting the validator to a shared pre-enqueue module
called from both submit surfaces.

An interim draft used a closed `INHERITABLE` enum (hardcoded list of
allowed secret names with explicit shadow-key sets and inline-cmd regex
scans). Codex flagged bypasses; the deeper question surfaced: what does the
closed enum actually defend on a single-uid topology? Same-uid trust = same
trust domain; refusing to let the agent inherit `voyage_api_key` because
it's not on a hand-curated list is paternalism. The shipped design opens
`inherit:` to any snake_case config-key. The agent decides.
