Profiles & Credentials — shared pattern reference
Reference skill describing the credential / profile / confirmation pattern used by all claude-team-toolkit skills (azure-devops, fastlane, firebase, heroku, k6, maestro, postgres, rspec, sentry, shopify, slack, trello, etc.). Implementation lives in lib/credentials.sh and lib/confirm.sh.
Overview
All credential-bearing skills share a uniform pattern:
- INI credentials at
~/.<service>/credentials(mode 600) - Multiple
[profile]sections per file (e.g.,[default],[work],[prod]) - Resolution order:
--profile <name>flag →<SERVICE>_PROFILEenv →~/.<service>/active_profile→[default] - Helper sourced from
lib/credentials.shpopulatesCTT_<KEY>env vars - Destructive ops gated by
ctt_confirmfromlib/confirm.sh - Actions audit-logged to
~/.claude-team-toolkit/audit.log(key NAMES only, never values)
When to Use
- Authoring a NEW skill that needs per-account/per-org/per-env switching
- Modifying credential loading in an existing skill
- Adding destructive operations that need typed confirmation
- Debugging why a skill picks the wrong profile
- Reviewing security posture of a skill
When NOT to Use
- One-off scripts not meant for reuse → just hardcode and move on
- Skills with no credentials (e.g.,
react-native,rails-security) → nothing to load - Cross-cloud unified credential systems → use the cloud's native config (
~/.aws/credentials, etc.) instead of reinventing
Profile file shape
# ~/.<service>/credentials (mode 600)
[default]
field_a = value
field_b = secret
[other-profile]
field_a = different
require_confirm = true # gate every mutation in this profile
After ctt_load_creds <service> [profile]:
CTT_PROFILE=<resolved>(e.g.,default)CTT_FIELD_A=<value>,CTT_FIELD_B=<secret>(uppercased, hyphens → underscores)
Each load clears every CTT_* variable before populating the new ones,
except the control variables CTT_NONINTERACTIVE and CTT_HOME. A field
present in the old profile but absent from the new one is therefore unset, not
inherited — without that, switching profiles would silently carry
default_team, org, project, default_app, database, read_only or
require_confirm across accounts, and across services. The names come from
the shell (compgen -v CTT_), not from a tracking variable: a tracking
variable is state a hostile environment can pre-set.
Two boolean fields are normalised at load: read_only and require_confirm
become the literal true for any of true, yes, 1, on (case-insensitive)
and false otherwise, so [ "$CTT_READ_ONLY" = "true" ] is the correct test in
every skill and read_only = yes cannot fail open.
ctt_save_profile refuses profile names outside [A-Za-z0-9_-], field names
outside [a-zA-Z0-9_-], and any value containing a newline — each of those
could otherwise write a second [section] into the INI file. ctt_load_creds
applies the same name rule to the resolved profile, because the name is used in
a grep pattern. ctt_audit_log replaces newlines and tabs in the action text so
an id or team key cannot forge a second audit record.
Consequence for skill authors: read every field as ${CTT_FIELD:-} and treat
unset as "not configured on this profile". Never assume a field survives a
profile switch.
Helper API (from lib/credentials.sh)
| Function | Purpose |
|---|---|
ctt_load_creds <svc> [profile] |
Clear the previous profile's fields, then resolve + load this one into CTT_* env |
ctt_save_profile <svc> <profile> <k=v>... |
Atomic INI section write |
ctt_list_profiles <svc> |
List sections, mark active with * |
ctt_use_profile <svc> <profile> |
Set active profile pointer |
ctt_active_profile <svc> |
Print resolved active profile |
ctt_remove_profile <svc> <profile> |
Refuses to remove last profile |
ctt_mask <value> |
Returns ****<last4> for safe logging |
ctt_validate_perms <file> |
Refuses non-600/400 mode (POSIX only) |
ctt_audit_log <svc> <action> |
Append timestamp + profile + action |
Confirmation API (from lib/confirm.sh)
ctt_confirm "Delete app foo?" || return 1 # y/N prompt
ctt_confirm "Drop table users?" "DELETE" || return 1 # typed phrase
ctt_warn_destructive "Action description" # noisy stderr banner
CTT_NONINTERACTIVE=1 auto-denies all confirmations (CI safety).
Common Mistakes
- Reading credentials directly with
awkinstead ofctt_load_creds→ bypasses perm validation and the cross-profile clearing - Assuming a field persists across a profile switch → it is cleared on every load. Read
${CTT_FIELD:-}, not$CTT_FIELD. - Logging full credential values instead of
ctt_mask→ secrets leak to terminal/CI logs - Using
evalto set vars from credentials → injection risk. Helper usesprintf -vfor safety. - Audit log records values not just key names → audit log itself becomes a secret store
- Confirmation without typed phrase for irreversible ops → "y" is too easy a typo
- Service name with uppercase or special chars → helper validates
^[a-z0-9-]+$
Security defaults
- Credentials directory:
chmod 700. File:chmod 600. Helper refuses to load otherwise (POSIX). - Windows: best-effort (NTFS ACLs not enforced by helper) — document in skill's safety section.
CTT_NONINTERACTIVE=1in CI → auto-deny destructive ops (no surprise mutations).- Audit log at
~/.claude-team-toolkit/audit.log(mode 600). Records: timestamp, service, profile, action. - Never write credentials into git, chat output, or any file outside
~/.<service>/credentials.