[cleo] claude config + new packages

This commit is contained in:
Matthew Bessette
2026-10-02 15:42:56 -04:00
parent d9ae284378
commit 71ae57dd92
11 changed files with 285 additions and 29 deletions
+12
View File
@@ -0,0 +1,12 @@
# Personal instructions
## Where code lives
- "The code", "the repo", "the monorepo", or any general code question refers to `~/Projects/cleo-infinite`.
- Anything mentioning Retool refers to `~/Projects/cleo-retool`.
## Worktrees
- Never write or edit code in the main checkouts (`~/Projects/cleo-infinite`, `~/Projects/cleo-retool`). Work in a treehouse worktree under `~/.treehouse/<repo>-<hash>/<slot>/<repo>`. Check with `git rev-parse --show-toplevel` and `git worktree list`.
- If code changes are needed and the current directory is not a treehouse worktree, start one yourself without asking first, then continue there. Read-only investigation can stay where it is.
- Treehouse is a zsh alias for `nix run github:kunchenguid/treehouse --`, so it is not on PATH in non-interactive shells. Run the full `nix run` form, and run it with `--help` first rather than guessing subcommands or flags.
- Branch names follow the repo's own rules (cleo-infinite: `<JIRA-KEY>-short_slug` from latest `origin/main`). Take the Jira key from the user or the current branch. If there isn't one, ask. Never invent a key.
- A pool slot sitting on a detached HEAD is not necessarily free. Don't reuse a slot that has uncommitted changes or that another session may be using.
+21
View File
@@ -0,0 +1,21 @@
{
"environment": [
"Organization: hicleo / Cleo (startwithlucy GitHub org; Jira at hicleo.atlassian.net)",
"Cloud provider: AWS, accessed via SSO with Granted (`assume`, `assume -c`)",
"Trusted repos: startwithlucy/cleo-infinite (origin) and git.thiccdata.io:mbessette/cleo-infinite.git (home). Both private, so confidential material is fine there, but secrets, PII and PHI are never cleared by visibility alone",
"No internal snippet hosting exists: treat public paste and gist services as outside the trust boundary",
"Secrets are held as CI secrets (CODECOMMIT_SSH_PRIVATE_KEY, CODECOMMIT_SSH_PRIVATE_KEY_ID, DD_API_KEY, GITHUB_TOKEN, JIRA_API_KEY, JIRA_EMAIL, SLACK_BOT_TOKEN, SNYK_KEY); never print or transmit their values",
"Default branch is main. The `Copilot Review` ruleset is active and 100+ named branches are protected, so treat branches as protected even where classic protection lists are empty",
"Trusted internal domain: hicleo.atlassian.net. sr71.sdet.hicleo.com (VPN-gated Jenkins) may be used interactively with $JENKINS_USER and $JENKINS_TOKEN from the shell env, never unattended",
"Routine CLIs: mise, yarn (Yarn 4 workspaces), buf, assume/Granted, gh, jira",
"Sensitive data locations: .env, .env.local, .env.test, .env.production and nested equivalents, *.key files, frontends/*/.env*, cicd/deploy-manager/chalicelib/code_prod_deploy_service.py, and infra-space/** terraform state and apply/plan/destroy scripts",
"Sensitive remote targets: any namespace, host or container with `prod` or `production` as a whole word or segment, including pre-prod-enroll and enroll.hicleo.com",
"Protected IaC scopes: infra-space/core/**, infra-space/environment/**, infra-space/applications/**, frontends/web-messaging/infra/**, plus IAM, RBAC, networking, quota and node-pool resources and anything named prod/production",
"Claude Code runs on an ordinary developer machine with open internet; no host containment or network restrictions"
],
"allow": [],
"soft_deny": [
"Bash(terraform destroy:*) in infra-space/** and frontends/web-messaging/infra/**: protected IaC, prod-adjacent destructive apply",
"Bash(rm:*) targeting infra-space/**/terraform.tfstate or any .tfstate file: irreversible state destruction"
]
}
+68
View File
@@ -0,0 +1,68 @@
---
name: prod-db
description: >-
Look up data in Cleo production read replicas (Postgres, IAM auth, read-only).
Use when the user asks to look up, find, or check something in prod / the prod
DB, e.g. a user id, family id, guide id, or other record IDs for a given
user. Returns IDs and metadata only, never PII/PHI.
user-invocable: true
argument-hint: '[question about prod data]'
---
# prod-db
Query prod read replicas through one script. Never call `psql` or `aws rds` directly.
```sh
~/.config/claude/skills/prod-db/prod-query.sh <alias> "<select sql>"
~/.config/claude/skills/prod-db/prod-query.sh <alias> --tables
~/.config/claude/skills/prod-db/prod-query.sh <alias> --columns <table>
```
The script connects to the `cluster-ro` endpoints as `reader`, forces read-only sessions with a 30s statement timeout, and caps output at 200 rows. If it reports no SSO session, ask the user to run `! aws sso login --profile prod-mono-reader`.
## PII/PHI rules (org policy: never put PII/PHI into Claude)
- SELECT only IDs, foreign keys, UUIDs, timestamps, statuses, flags and counts.
- Never select names, emails, phones, DOB, addresses, notes, message bodies, health data, or Kratos `traits`. Never `SELECT *`.
- A value the user gives you (an email) may appear only in a `WHERE` clause. Do not repeat it in your output; refer to "the user".
- The script blocks `*`, non-SELECT statements, and result columns whose names look sensitive (it prints only the offending column names). Aliasing a sensitive column to dodge this is forbidden. If the user needs a PII field, tell them to look it up in Postico.
- Report results as IDs only.
## Workflow
1. Pick the alias. Run `--tables` / `--columns <table>` (schema only) before writing SQL for an unfamiliar table.
2. Write a narrow SELECT with explicit columns and a `LIMIT`.
3. Summarize the IDs. Don't paste raw output beyond what's needed.
## Aliases
backend (backenddb), guides, kratos, userflow (userpreferencesdb), scheduling (schedulingdb), messaging (messagingdb), eligibility (eligibilitydb), entitlements (entitlementsdb), benefits (benefitsdb), assessments (assessmentsdb), engagement (engagementdb), tagging (taggingdb), supportplan (supportplandb), integrations, content (thegoodcontentdb), retool.
## Schema notes
**backend** (Django app `lucy_web`, `public` schema):
- `auth_user(id, email)` is the user.
- `lucy_web_family(id, employee_user_id, partner_user_id)` links a user to a family, as employee or partner.
- `lucy_web_childfamilypackage(id, family_id, child_id, guide_id, guide_assigned_on)` links a family to its guide. The current guide is the row with the highest `id` for the family (see `Family.lucy_guide` in `services/cleo-backend/lucy_web/models/family.py`).
- `lucy_web_lucyguide(id, uuid, user_id, is_active)` is the guide. `user_id` is the guide's own `auth_user` row. Do not select the guide's name or email.
- Caregiver users are linked through the caregiver childfamilypackage models, which this query does not cover.
**guides**: tables store a numeric `guide_id` equal to `lucy_web_lucyguide.id`. It does not own the user-to-guide assignment.
**kratos**: `identities(id uuid, traits jsonb)`. The Kratos id is not known to equal backend `auth_user.id`. Bridge on email, inside `WHERE` only. Unverified for this deployment.
## Example: user id and guide id for an email
```sql
select u.id as user_id, f.id as family_id,
g.id as guide_id, g.uuid as guide_uuid, cfp.guide_assigned_on
from auth_user u
join lucy_web_family f on f.employee_user_id = u.id or f.partner_user_id = u.id
join lucy_web_childfamilypackage cfp on cfp.family_id = f.id
join lucy_web_lucyguide g on g.id = cfp.guide_id
where lower(u.email) = lower('<email>')
order by cfp.id desc
limit 5
```
Run it with alias `backend`.
+16
View File
@@ -0,0 +1,16 @@
backend prod-backend-aurora-cluster.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com backenddb
guides prod-guides.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com guides
kratos prod-orykratos.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com kratos
userflow prod-userflow.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com userpreferencesdb
scheduling prod-scheduling.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com schedulingdb
messaging prod-messaging.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com messagingdb
eligibility prod-eligibility.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com eligibilitydb
entitlements prod-entitlements.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com entitlementsdb
benefits prod-benefits.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com benefitsdb
assessments prod-assessments.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com assessmentsdb
engagement prod-engagement.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com engagementdb
tagging prod-tagging.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com taggingdb
supportplan prod-supportplan.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com supportplandb
integrations prod-platformintegrations.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com integrations
content prod-content.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com thegoodcontentdb
retool prod-retoolpub.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com retool
1 backend prod-backend-aurora-cluster.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com backenddb
2 guides prod-guides.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com guides
3 kratos prod-orykratos.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com kratos
4 userflow prod-userflow.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com userpreferencesdb
5 scheduling prod-scheduling.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com schedulingdb
6 messaging prod-messaging.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com messagingdb
7 eligibility prod-eligibility.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com eligibilitydb
8 entitlements prod-entitlements.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com entitlementsdb
9 benefits prod-benefits.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com benefitsdb
10 assessments prod-assessments.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com assessmentsdb
11 engagement prod-engagement.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com engagementdb
12 tagging prod-tagging.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com taggingdb
13 supportplan prod-supportplan.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com supportplandb
14 integrations prod-platformintegrations.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com integrations
15 content prod-content.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com thegoodcontentdb
16 retool prod-retoolpub.cluster-ro-czxwiektwmk0.us-east-1.rds.amazonaws.com retool
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Read-only lookups against prod read replicas (IAM auth). IDs/metadata only.
#
# prod-query.sh <alias> "<select sql>"
# prod-query.sh <alias> --tables
# prod-query.sh <alias> --columns <table>
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROFILE="prod-mono-reader"
REGION="us-east-1"
DB_USER="reader"
MAX_ROWS=200
DENY_COLS='email|name|phone|birth|dob|address|street|zip|postal|ssn|traits|note|body|content|message|text|diagnos|medical|password|token|secret'
die() { echo "prod-query: $*" >&2; exit 1; }
[[ $# -ge 2 ]] || die "usage: prod-query.sh <alias> \"<select sql>\" | --tables | --columns <table>"
alias_name="$1"; shift
row="$(awk -F'\t' -v a="$alias_name" '$1 == a { print; exit }' "$DIR/databases.tsv")"
[[ -n "$row" ]] || die "unknown alias '$alias_name'. Known: $(cut -f1 "$DIR/databases.tsv" | paste -sd' ' -)"
host="$(cut -f2 <<<"$row")"
database="$(cut -f3 <<<"$row")"
internal=0
case "$1" in
--tables)
internal=1
sql="select table_schema, table_name from information_schema.tables where table_schema not in ('pg_catalog','information_schema') order by 1, 2"
;;
--columns)
[[ $# -eq 2 ]] || die "--columns needs a table name"
[[ "$2" =~ ^[A-Za-z0-9_.]+$ ]] || die "invalid table name"
internal=1
sql="select column_name, data_type from information_schema.columns where table_name = '${2##*.}' order by ordinal_position"
;;
*)
sql="$1"
;;
esac
if [[ $internal -eq 0 ]]; then
sql="$(sed -E 's/[[:space:]]*;[[:space:]]*$//' <<<"$sql")"
lc="$(tr '[:upper:]' '[:lower:]' <<<"$sql" | tr '\n' ' ')"
[[ "$lc" =~ ^[[:space:]]*(select|with)[[:space:]] ]] || die "only a single SELECT/WITH statement is allowed"
[[ "$sql" != *";"* ]] || die "multiple statements are not allowed"
[[ ! "$lc" =~ (^|[^a-z_])(insert|update|delete|drop|alter|create|grant|revoke|truncate|copy|call|do|set|reset|lock|vacuum|pg_read_file|pg_ls_dir|dblink|lo_import|lo_export)([^a-z_]|$) ]] \
|| die "statement contains a disallowed keyword"
stripped="$(sed -E 's/count\([[:space:]]*\*[[:space:]]*\)//g' <<<"$lc")"
[[ ! "$stripped" =~ (select|,)[[:space:]]*(distinct[[:space:]]+)?([a-z_0-9]+\.)?\* ]] \
|| die "'*' in the select list is not allowed; list explicit ID/metadata columns (use --columns <table>)"
fi
aws sts get-caller-identity --profile "$PROFILE" >/dev/null 2>&1 \
|| die "no valid AWS SSO session. Run: ! aws sso login --profile $PROFILE"
PGPASSWORD="$(aws --profile "$PROFILE" rds generate-db-auth-token \
--hostname "$host" --port 5432 --region "$REGION" --username "$DB_USER")"
export PGPASSWORD
export PGHOST="$host" PGPORT=5432 PGDATABASE="$database" PGUSER="$DB_USER" PGSSLMODE=require
export PGOPTIONS="-c default_transaction_read_only=on -c statement_timeout=30000"
out="$(mktemp)"
trap 'rm -f "$out"' EXIT
psql -X --csv -v ON_ERROR_STOP=1 -c "$sql" >"$out"
header="$(head -n 1 "$out")"
if [[ $internal -eq 0 ]]; then
bad="$(tr ',' '\n' <<<"$header" | tr -d '"' | grep -Ei "$DENY_COLS" || true)"
if [[ -n "$bad" ]]; then
echo "prod-query: blocked, output columns look like PII/PHI (rows not shown):" >&2
echo "$bad" >&2
echo "Select IDs/metadata only; use these fields only inside WHERE." >&2
exit 2
fi
fi
total=$(( $(wc -l <"$out") - 1 ))
head -n $(( MAX_ROWS + 1 )) "$out" \
| sed -E 's/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/<redacted-email>/g'
if (( total > MAX_ROWS )); then
echo "prod-query: output truncated to $MAX_ROWS of $total rows" >&2
fi
+18 -4
View File
@@ -71,9 +71,13 @@ in
ttyd
xcodes
xz
# node 22 pinned to the exact version cleo-app requires (nixpkgs-nodejs in flake.nix)
pinnedNode.nodejs_22
pinnedNode.yarn
# node 24.x from unstable (cached); the pinnedNode 24.19.0 rev had no cache.nixos.org binaries
unstable.nodejs_24
unstable.yarn
# iOS simulator UI driving (tap/scroll/type) for screenshots and e2e flows
unstable.maestro
unstable.idb-companion
act
];
# Add Homebrew to PATH
@@ -147,6 +151,16 @@ in
);
};
nix.settings.experimental-features = "nix-command flakes";
nix = {
settings = {
experimental-features = "nix-command flakes";
auto-optimise-store = true;
};
gc = {
automatic = true;
interval = { Weekday = 0; Hour = 2; Minute = 0; }; # Sunday at 2:00 AM
options = "--delete-older-than 7d";
};
};
system.stateVersion = 4;
}
+23
View File
@@ -1,4 +1,10 @@
{ config, pkgs, vars, ... }:
let
# Agent/skill bodies live in the cleo-infinite checkout; symlinked so they
# follow upstream on every pull.
ansibleFiles = "${config.home.homeDirectory}/Projects/cleo-infinite/scripts/local-dev-ansible/files";
linkAnsibleFile = name: config.lib.file.mkOutOfStoreSymlink "${ansibleFiles}/${name}";
in
{
imports = [
./programs.vscode.nix
@@ -96,6 +102,23 @@
};
};
# Claude Code (tasks/config/claude-code.yml). The remaining settings keys
# (additionalDirectories, statusLine, ...) come from home.shared.nix.
my.claude.settings = {
awaySummaryEnabled = false;
promptSuggestionEnabled = false;
# Generated by /auto-mode-setup; kept as JSON so it can be re-pasted and diffed.
autoMode = builtins.fromJSON (builtins.readFile ./claude/auto-mode.json);
};
xdg.configFile."claude/agents/Explore.md".source = linkAnsibleFile "claude-explore-agent.md";
xdg.configFile."claude/agents/orchestrator-sonnet.md".source = linkAnsibleFile "claude-orchestrator-sonnet-agent.md";
xdg.configFile."claude/agents/orchestrator-opus.md".source = linkAnsibleFile "claude-orchestrator-opus-agent.md";
xdg.configFile."claude/skills/orchestrate/SKILL.md".source = linkAnsibleFile "claude-orchestrate-skill.md";
xdg.configFile."claude/CLAUDE.md".source =
config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nixos/claude/CLAUDE.md";
xdg.configFile."claude/skills/prod-db".source =
config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nixos/claude/skills/prod-db";
# opencode (tasks/config/opencode.yml)
xdg.configFile."opencode/opencode.json".text = builtins.toJSON {
"$schema" = "https://opencode.ai/config.json";
Generated
+4 -4
View File
@@ -254,17 +254,17 @@
},
"nixpkgs-nodejs": {
"locked": {
"lastModified": 1737545071,
"narHash": "sha256-j4HeaLw1LZxkCvuOxdO1xTnPYLSOQuzOjGEuCK80X2w=",
"lastModified": 1785767437,
"narHash": "sha256-1FG0hlMWqs1Yh3NJzt5ejMRqhJdSsr1qMqiAgMO3ZKs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "21808d22b1cda1898b71cf1a1beb524a97add2c4",
"rev": "700ca73b459b479645c7e5a957def679414c886f",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "21808d22b1cda1898b71cf1a1beb524a97add2c4",
"rev": "700ca73b459b479645c7e5a957def679414c886f",
"type": "github"
}
},
+2 -2
View File
@@ -38,8 +38,8 @@
nix-flatpak.url = "github:gmodena/nix-flatpak/main";
# Pinned nixpkgs for a specific nodejs_22 + yarn version required by cleo-app
nixpkgs-nodejs.url = "github:NixOS/nixpkgs/21808d22b1cda1898b71cf1a1beb524a97add2c4";
# Pinned nixpkgs for a specific nodejs_24 (24.19.0) + yarn version required by cleo-app
nixpkgs-nodejs.url = "github:NixOS/nixpkgs/700ca73b459b479645c7e5a957def679414c886f";
};
outputs =
+4 -4
View File
@@ -15,6 +15,8 @@ let
rebuildCmd = if pkgs.stdenv.isDarwin then "darwin-rebuild" else "nixos-rebuild";
in
{
imports = [ ./module.claude.nix ];
home.stateVersion = lib.mkDefault "26.05";
home.username = userConfig.username;
home.homeDirectory = userConfig.homeDirectory;
@@ -41,8 +43,7 @@ in
vault = "${userConfig._1PassKeyVault}"
'';
xdg.configFile."claude/settings.json".source = pkgs.writeText "claude-settings.json" (
builtins.toJSON {
my.claude.settings = {
"$schema" = "https://json.schemastore.org/claude-code-settings.json";
theme = "dark";
agent = "plan";
@@ -58,8 +59,7 @@ in
type = "command";
command = "claude-statusline prompt";
};
}
);
};
xdg.configFile."claude-statusline/config.toml".text = ''
format = "$directory | $git_branch | $model | $cost | $context | $usage"
+16
View File
@@ -0,0 +1,16 @@
{ config, lib, pkgs, ... }:
let
jsonFormat = pkgs.formats.json { };
in
{
# Host modules contribute keys here; the module system deep-merges them into
# a single ~/.config/claude/settings.json.
options.my.claude.settings = lib.mkOption {
type = jsonFormat.type;
default = { };
description = "Claude Code settings.json contents.";
};
config.xdg.configFile."claude/settings.json".source =
jsonFormat.generate "claude-settings.json" config.my.claude.settings;
}