Config Studio for Claude Code
Configure
Permissions
not in the planobservedRules deciding whether a tool call runs, stops to ask, or is refused outright.
Here you set: What it may do without asking, what it must ask about, and what it may never do.
Where it lives ยท 4 filesยท highest precedence first
- 1
/etc/claude-code/managed-settings.json - 2
.claude/settings.local.json - 3
.claude/settings.json - 4
~/.claude/settings.json
Changes
What it writes
emptyNothing is in the plan yet. Tick a surface in the rail to add what you have set to it; the plan is every file, across every tool, in one review.
Try it
Would this be allowed?
permissions.decidePut a real tool call to the real matcher, with your own rules.
Your input
- deny
- ask
- allow
- Everything unmatched
- The call
Answer
deny
`Bash(rm:*)` in the deny list matched first, so it decided.
Does this one rule match?
permissions.ruleMatchesThe narrower question, for when a rule looks right and does nothing.
Your input
- The rule
- The call
- In which list
Answer
does not match
`Bash(npm run test:*)` does not match this call in the allow list.
Which file is deciding this?
settings.mergeFour files can set the same key. This says which one won, what it shadowed, and what was ignored.
Your input
- The key
- managed-settings.json
- .claude/settings.local.json
- .claude/settings.json
- ~/.claude/settings.json
Answer
combined
permissions combines rather than being overridden, so all of .claude/settings.json and ~/.claude/settings.json are in force at once. Nothing here is shadowed.
{
"deny": [
"Bash(rm:*)"
],
"allow": [
"Read"
]
}Turn it off
killswitch.containFor the ten minutes when something is wrong and you want it stopped rather than understood. Pick what to cut and this is the exact block, with what each one costs you.
Your input
- What to stop
Answer
1 off
1 lever, folded into one block for a managed policy. It goes in the administrator's file rather than yours, because a switch a user can undo is not a switch. It goes in /etc/claude-code/managed-settings.json on Linux.
- Stops: Stop every hook: Every hook in every settings file, including any a cloned repository brought with it. A hook is a shell command that runs without a prompt, so this is the one that closes arbitrary execution you did not ask for.
- Costs: Stop every hook: Your own hooks stop too: formatters, audit logs, the secret scanner. The statusline goes with them, because Claude Code disables both together.
{
"disableAllHooks": true
}What is actually set up here?
setup.auditPoint this at your own ~/.claude, or at a project's .claude, and it reports what is configured and what that leaves open. Read in your browser, nothing uploaded, nothing kept.
Your input
- A .claude folder
- Which one is it
Answer
no folder yet
Pick a .claude folder and what it contains is reported here.
What actually loads?
memory.assembleInstruction files at four scopes, plus whatever the @ imports pull in, or silently do not.
Your input
- ~/.claude/CLAUDE.md
- ./CLAUDE.md
- Files that exist
Answer
3 files
2 instruction files load, in the order managed, user, project, local, and all of them stay in context together. 1 more arrives through @ imports.
Be brief. @./house-style.md (the contents of ./house-style.md) Use tabs.
Is the habit in the prompt, or only in your head?
prompt.lintSix structures a prompt either has or does not. It says nothing about whether the prompt works, only whether the thing you meant is written down.
Your input
- The prompt
Answer
2 of 6
This underspecifies the job. Each note below is a question the model will answer for you if you do not.
- โ Says what to do: Opens with the thing you want done.
- โ Instructions and data are separated: Instructions and material run together. Put the material in tags or a fenced block, so a document that contains the word "summarise" cannot be read as part of the request.
- โ Shows an example: No example. Two short worked examples settle more questions about the shape you want than a paragraph describing it.
- โ Says what the answer should look like: Nothing about the shape of the answer, so you get whichever shape the model picks and a different one next time.
- โ No word doing the work of a number: Nothing in it is left to taste.
- โ Says what not to do, or where to stop: No boundary. A prompt with no limit and no exclusion is answered at whatever length the model feels, which is the complaint people usually arrive with.
Can anybody read that?
contrast.paletteA status line that looks washed out is not a taste problem. It is a ratio, and the fix is one number away.
Your input
- Terminal background
- The colours you use
Answer
4 too faint
4 of 7 do not reach 4.5:1 against that background: grey, magenta, blue, red. grey is the worst at 2.87:1, and it is the one that will disappear first.
- grey #555753, 2.87:1, fails. #747572 would pass.
- magenta #75507b, 3.19:1, large-only. #8a6a8f would pass.
- blue #3465a4, 3.54:1, large-only. #4c77af would pass.
- red #cc0000, 3.57:1, large-only. #d73838 would pass.
- green #4e9a06, 5.95:1, passes
- cyan #06989a, 5.97:1, passes
- yellow #c4a000, 8.38:1, comfortable
What exactly gets written to produce it?
statusline.scriptThe bar is a program. This is the whole of it, from a template you pick, in the language you would run it in.
Your input
- Start from
- Written in
Answer
98 lines
This is written by the same code that draws the preview, so the bar you see and the script you take away cannot disagree.
#!/usr/bin/env bash
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Claude Code status line ยท generated by BENIBIX
# skin: custom ยท palette: Claude ยท shape: flow ยท lines: 1
# Claude Code pipes a JSON payload on stdin.
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# ---------- palette ----------
R=$'\033[0m'
C_fg=$'\033[38;2;230;232;238m'
C_dim=$'\033[38;2;107;114;128m'
C_muted=$'\033[38;2;154;161;177m'
C_accent=$'\033[38;2;232;132;107m'
C_good=$'\033[38;2;127;209;147m'
C_warn=$'\033[38;2;229;192;123m'
C_bad=$'\033[38;2;224;108;117m'
C_info=$'\033[38;2;124;171;232m'
C_special=$'\033[38;2;199;146;234m'
# ---------- heat ramp (green to red) ----------
RAMP=($'\033[38;2;63;212;127m' $'\033[38;2;92;217;111m' $'\033[38;2;142;222;92m' $'\033[38;2;194;224;79m' $'\033[38;2;232;209;63m' $'\033[38;2;240;176;58m' $'\033[38;2;240;138;53m' $'\033[38;2;236;99;48m' $'\033[38;2;229;68;46m' $'\033[38;2;224;32;32m')
pct_color(){ local p=${1%%.*}; [[ $p =~ ^-?[0-9]+$ ]] || p=0; [ "$p" -gt 100 ] && p=100; [ "$p" -lt 0 ] && p=0; printf %s "${RAMP[$(( p * 9 / 100 ))]}"; }
pct_color_inv(){ local p=${1%%.*}; [[ $p =~ ^-?[0-9]+$ ]] || p=0; pct_color $(( 100 - p )); }
# ---------- helpers ----------
join_seg(){ local sep="$1"; shift; local out="" s sp=0
for s in "$@"; do
[ -z "$s" ] && continue
if [ -z "${s// /}" ]; then out+="$s"; sp=1; continue; fi # a spacer: no separator around it
[ -n "$out" ] && [ "$sp" = "0" ] && out+="$sep"
out+="$s"; sp=0
done; printf %s "$out"; }
time_left(){ local ts=${1%%.*} now left h m
[ "$ts" -gt 0 ] 2>/dev/null || return
now=$(date +%s); left=$(( ts - now ))
[ "$left" -le 0 ] && { printf %s "now"; return; }
h=$(( left / 3600 )); m=$(( (left % 3600) / 60 ))
if [ "$h" -ge 24 ]; then printf "%dd%dh" $(( h / 24 )) $(( h % 24 ))
elif [ "$h" -gt 0 ]; then printf "%dh%02dm" "$h" "$m"
else printf "%dm" "$m"; fi; }
money(){ awk -v c="$1" -v f="$2" 'BEGIN{ if(c=="") exit
c=c+0
if(f=="cents") printf "%.0fยข", c*100; else if(f=="round") printf "$%.0f", c; else printf "$%.2f", c }'; }
round(){ awk -v n="$1" 'BEGIN{ if(n=="") exit
n=n+0; printf "%d", (n>=0 ? int(n+0.5) : -int(-n+0.5)) }'; }
strip_paren(){ printf %s "${1%% (*}"; }
# ---------- payload ----------
# lengths and cuts count characters only in a UTF-8 locale, which some setups leave out
_u='รฉ'; [ ${#_u} = 1 ] || { for _l in C.UTF-8 en_US.UTF-8; do export LC_ALL=$_l; [ ${#_u} = 1 ] && break; done; [ ${#_u} = 1 ] || unset LC_ALL; } 2>/dev/null
input=$(cat)
# Fall back to an empty object if stdin is empty or not a JSON object,
# so a stray invocation prints a bare line instead of a jq error.
printf %s "$input" | jq -e 'type == "object"' >/dev/null 2>&1 || input='{}'
command -v jq >/dev/null 2>&1 || { printf %s 'This status line needs jq (jqlang.org). Install it, then restart Claude Code.'; exit 0; }
F=()
while IFS= read -r _l; do F+=("$_l"); done < <(printf %s "$input" | jq -r '
((try (.model.display_name // "Claude") catch "Claude") | if type == "object" or type == "array" then tojson elif type == "string" then gsub("[\r\n]"; " ") else . end), # F_model_display_name
((try (.cost.total_cost_usd // 0) catch 0) | if type == "object" or type == "array" then tojson elif type == "string" then gsub("[\r\n]"; " ") else . end), # F_cost_total_cost_usd
((try (.rate_limits.five_hour.resets_at // 0) catch 0) | if type == "object" or type == "array" then tojson elif type == "string" then gsub("[\r\n]"; " ") else . end), # F_rate_limits_five_hour_resets_at
((try (.rate_limits.five_hour.used_percentage // "") catch "") | if type == "object" or type == "array" then tojson elif type == "string" then gsub("[\r\n]"; " ") else . end), # F_rate_limits_five_hour_used_percentage
((try (.rate_limits.seven_day.used_percentage // "") catch "") | if type == "object" or type == "array" then tojson elif type == "string" then gsub("[\r\n]"; " ") else . end) # F_rate_limits_seven_day_used_percentage
' | tr -d '\r')
F_model_display_name=${F[0]}
F_cost_total_cost_usd=${F[1]}
F_rate_limits_five_hour_resets_at=${F[2]}
F_rate_limits_five_hour_used_percentage=${F[3]}
F_rate_limits_seven_day_used_percentage=${F[4]}
# ---------- segments ----------
A1L=()
v1l1="$(strip_paren "$F_model_display_name")"
if [ -n "$v1l1" ]; then
A1L+=("${C_accent}โ ${C_accent}${v1l1}${R}")
fi
v1l2="$(money "$F_cost_total_cost_usd" 'usd')"
if [ -n "$v1l2" ]; then
A1L+=("${C_good}${v1l2}${R}")
fi
v1l3="$(time_left "$F_rate_limits_five_hour_resets_at")"
if [ -n "$v1l3" ]; then
A1L+=("${C_muted}โป ${C_muted}${v1l3}${R}")
fi
v1l4="$(round "$F_rate_limits_five_hour_used_percentage")"
if [ -n "$v1l4" ]; then
A1L+=("$(pct_color "$v1l4")โ ${C_dim}5H $(pct_color "$v1l4")${v1l4}$(pct_color "$v1l4")%${R}")
fi
v1l5="$(round "$F_rate_limits_seven_day_used_percentage")"
if [ -n "$v1l5" ]; then
A1L+=("$(pct_color "$v1l5")โ ${C_dim}7D $(pct_color "$v1l5")${v1l5}$(pct_color "$v1l5")%${R}")
fi
# ---------- assemble ----------
SEP="${C_dim} โ ${R}"
OUT1="$(join_seg "$SEP" "${A1L[@]}")"
# ---------- output ----------
printf '%s' "$OUT1"Would this hook run?
hooks.fireMatchers are compared differently per event, and a hook that never fires says nothing about it.
Your input
- Event
- Matcher
- Command
- And the thing happening is
Answer
it does not run
Nothing is configured for PreToolUse that matches, so nothing runs.
- Edit|Write is a list of exact names, and MultiEdit is not one of them.
Will this ever get picked?
authoring.coachThe description is the only thing read when deciding whether to delegate.
Your input
- The description
Answer
weak
This will probably not get picked. Work through the notes below. 2 of 6 checks pass.
- โ Too short. One clause rarely says both what it does and when to use it.
- โ Does not say when to use it. Add a sentence starting "Use when..." or "Use after...".
- โ Starts with what it does.
- โ Nothing concrete in it. Name the file types, the tools or the artefacts it deals with.
- โ Contains a vague word ("helps", "various", "things"). Say the specific thing instead.
- โ Gets straight to it.
Will this name resolve?
authoring.checkNameA name the tool cannot resolve fails at the moment you need the thing, not at the moment you write it.
Your input
- The name
Answer
will not resolve
A colon is reserved for plugin namespacing, so a name containing one will not resolve.
What would this take away?
keys.resolveThe cost of a binding is invisible at the time you make it.
Your input
- The stroke
- On which screen
Answer
3 in use
Ctrl+R already does 3 things: Open history search (Global), Next match (HistorySearch), Rename the selected session (Agents). Binding it takes those away.
- Global: Open history search
- HistorySearch: Next match
- Agents: Rename the selected session
Every screen this stroke is used on
keys.whatIsBoundOne stroke can mean four things depending on where you are, and rebinding it takes all four.
Your input
- The stroke
Answer
3 uses
Ctrl+C already does something on 3 screens, and a binding takes every one of them.
- Global: Cancel what is running
- Transcript: Leave the transcript
- HistorySearch: Give up
How would a person write this down?
keys.prettyThe form a config file wants and the form you would put in a README are not the same, and the difference is where typos live.
Your input
- The stroke
Answer
Shift+Cmd+P
Written down, that stroke reads as Shift+Cmd+P, which is the form to use in anything a person will read.
Shift+Cmd+P
Would this run unattended?
headless.planThe flag combinations that produce a job which looks fine and stalls.
Your input
- The prompt
- Permission mode
- Spend cap
- Output
Answer
schedulable
Bounded, unattended, and nothing asks. This is the shape a scheduled run should have.
claude -p 'review the diff and report' --output-format json --permission-mode plan --max-budget-usd 5
What is the command for it?
headless.commandThe flags you did not write are the ones that decide what happens when nobody is watching. This is the whole invocation, with nothing left implicit.
Your input
- The prompt
- Output format
- Permission mode
- Model
- Allowed tools
Answer
14 words
This is the command that would run, built by the same function the plan uses, so what you read here is what it writes.
claude -p 'review the diff and report' --output-format json --permission-mode acceptEdits --allowedTools 'Read Grep'
When does this fire?
cron.nextCron accepts almost anything and then runs on a schedule you did not mean.
Your input
- The expression
- Starting from
Answer
6 next
At 9:00am, on Monday, on the 1st of the month, (either one is enough, which is how cron works). Times are your local timezone.
- 8/17/2026, 9:00:00 AM
- 8/24/2026, 9:00:00 AM
- 8/31/2026, 9:00:00 AM
- 9/1/2026, 9:00:00 AM
- 9/7/2026, 9:00:00 AM
- 9/14/2026, 9:00:00 AM
What does this expression say?
cron.describeFive fields nobody reads twice. Said back in words you can check against what you meant.
Your input
- The expression
Answer
in words
Every 15 minutes from 9:00am to 5:45pm, on Monday, Tuesday, Wednesday, Thursday and Friday. Times are your local timezone.
How often is that, before I find out?
cron.runsPerMonthA schedule is a bill. Every run costs, and the difference between a good expression and a typo is two orders of magnitude.
Your input
- The expression
- Counting from
Answer
8,640 a month
Every 5 minutes. Times are your local timezone. That is about 8,640 runs a month.
What did that session cost?
transcript.readOpen a transcript from ~/.claude/projects and see the tokens, the models and the money. It is read in your browser, nothing is uploaded, and nothing is kept.
Your input
- A transcript
- Price it as at
Answer
no file yet
Pick a transcript and the numbers appear here.