Shows the permission mode and picks the one new sessions start in (permissions.defaultMode). Does nothing until you pick something.

Pick the permission mode that new sessions start in (permissions.defaultMode in your user settings.json) from one small pane, or from /permmode. It does nothing until you pick something: installing it writes no file.
/permmode opens the pane: the six modes (ask, plan, accept edits, auto, don't ask, bypass permissions) with the current default marked./permmode <mode> sets the default directly (default, plan, acceptEdits, auto, dontAsk, bypassPermissions); any other argument prints the usage. bypassPermissions is the exception: it can only be picked in the pane, so a model calling the command cannot turn every check off.unknown; switch the running mode with shift+tab. The mod never changes the running session's mode.bypassPermissions skips every permission check. While it is the default, the pane shows the warning "Bypass skips every permission check in every new session."What it writes: only permissions.defaultMode; every other key of settings.json is kept. The first change saves the original file as settings.json.permissions.bak, and every default it replaces is appended to settings.json.permissions.prev.bak. An invalid settings.json is never overwritten (a toast says so). The file is $CLAUDE_CONFIG_DIR/settings.json, else ~/.claude/settings.json.
/permissions is a Claude Code built-in, hence the name /permmode.
Works on its own, or together with the optional menu mod: then the menu lists "Permissions" and shows this page in its pane.
/plugin marketplace add danielmalka/malka-claude-mods
/plugin install permissions@malka-claude-mods
/plugin uninstall permissions@malka-claude-mods
The mode you picked stays in settings.json; to undo it, pick another with /permmode default (or edit permissions.defaultMode by hand). settings.json.permissions.bak is a last-resort copy of the file as it was before the first pick; restoring it drops every later change.
Type declarations in .claude-plugin/types/ are generated by Claude Code when the mod loads (not committed); the mod's own state contract is types/index.d.ts, and types/menu.d.ts vendors the menu mod's contract (apiVersion 1). Then claude plugin test plugins/permissions.
Escolha o modo de permissão em que novas sessões começam (permissions.defaultMode no settings.json do usuário) num painel pequeno ou com /permmode. Não faz nada até você escolher algo: instalar não grava arquivo nenhum.
/permmode abre o painel: os seis modos (ask, plan, accept edits, auto, don't ask, bypass permissions), com o padrão atual marcado./permmode <modo> define o padrão direto (default, plan, acceptEdits, auto, dontAsk, bypassPermissions); qualquer outro argumento mostra o uso. bypassPermissions é a exceção: só pode ser escolhido no painel, para que um modelo chamando o comando não desligue todas as verificações.unknown; troque o modo atual com shift+tab. O mod nunca muda o modo da sessão em andamento.bypassPermissions pula toda verificação de permissão. Enquanto ele for o padrão, o painel mostra o aviso "Bypass skips every permission check in every new session." (o texto é exibido em inglês, como os demais rótulos do painel).O que grava: só permissions.defaultMode; as demais chaves do settings.json ficam intactas. A primeira alteração guarda o arquivo original em settings.json.permissions.bak, e cada padrão substituído é acrescentado a settings.json.permissions.prev.bak. Um settings.json inválido nunca é sobrescrito (um aviso informa). O arquivo é $CLAUDE_CONFIG_DIR/settings.json, senão ~/.claude/settings.json.
/permissions é um comando nativo do Claude Code, daí o nome /permmode.
Funciona sozinho ou com o mod opcional menu (que lista "Permissions" e mostra a página no seu painel).
Instalar: /plugin marketplace add danielmalka/malka-claude-mods e depois /plugin install permissions@malka-claude-mods. Desinstalar: /plugin uninstall permissions@malka-claude-mods. O modo escolhido continua no settings.json; para desfazer, escolha outro com /permmode default (ou edite permissions.defaultMode à mão). settings.json.permissions.bak é uma cópia de último recurso do arquivo como estava antes da primeira escolha; restaurá-la descarta todas as mudanças posteriores.
hooks/register.tsx 129 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type {} from '../types/menu'
5import type { MenuEntry } from '../types'
6import { isMode, MODES, modeLabel, type Mode } from './modes'
7import { BYPASS_WARNING, permissionsPage } from './permissions-page'
8import { configDir, currentDefault, parseSettings, why, withDefaultMode, type Settings } from './settings'
9
10const defaultModeAtom = atom({ plugin: 'permissions', key: 'defaultMode' } as const, 'default')
11// Foreign ref: the optional `menu` mod owns it (apiVersion 1); undefined = no menu.
12const menuPanel = { plugin: 'menu', key: 'panel' } as const
13const menuEntryRef = { plugin: 'permissions', key: 'menuEntry' } as const
14
15const PANE = 'permissions-settings'
16const MENU_PANE = 'mods-menu'
17
18// ~/.claude/settings.json ----------------------------------------------------------------
19// Parsed first, so an invalid file is never overwritten. The first change keeps the original text in
20// settings.json.permissions.bak; a defaultMode a pick supersedes is appended to settings.json.permissions.prev.bak.
21
22async function settingsPath($: EngineInterface) {
23 const home = (await $.env.get('HOME')) || (await $.env.get('USERPROFILE'))
24 return `${configDir(await $.env.get('CLAUDE_CONFIG_DIR'), home)}/settings.json`
25}
26
27// Two quick presses cannot interleave their file writes.
28let queue: Promise<unknown> = Promise.resolve()
29function serial<T>(job: () => Promise<T>): Promise<T> {
30 const result = queue.then(job)
31 queue = result.catch(() => undefined)
32 return result
33}
34
35async function writeDefaultMode($: EngineInterface, mode: Mode) {
36 const path = await settingsPath($)
37 const existed = await $.fs.exists(path)
38 const text = existed ? ((await $.fs.read(path)) as string) : '{}'
39 const settings: Settings = parseSettings(text)
40 const next = withDefaultMode(settings, mode)
41 const prev = currentDefault(settings)
42 if ((prev ?? 'default') === mode) return // already the default (an absent key means default): nothing to write, no backup
43 // the safe copies first, the settings.json write last: a failure never loses the user's value
44 if (prev !== undefined && prev !== mode) {
45 const prevPath = `${path}.permissions.prev.bak`
46 const old = (await $.fs.exists(prevPath)) ? parseSettings((await $.fs.read(prevPath)) as string, 'the permissions history file') : {}
47 const history: { defaultMode: string; at: string }[] = Array.isArray(old.history) ? old.history : []
48 history.push({ defaultMode: prev, at: new Date(await $.clock.now()).toISOString() })
49 await $.fs.write(prevPath, `${JSON.stringify({ history }, null, 2)}\n`)
50 }
51 const backup = `${path}.permissions.bak`
52 if (existed && !(await $.fs.exists(backup))) await $.fs.write(backup, text)
53 await $.fs.write(path, `${JSON.stringify(next, null, 2)}\n`)
54}
55
56// permissions.defaultMode: the mode new sessions start in. A running session's mode is shift+tab's; a mod cannot set it.
57async function setDefaultMode($: EngineInterface, mode: Mode) {
58 await serial(() => writeDefaultMode($, mode))
59 await update($, defaultModeAtom, () => mode)
60 await writeEntry($)
61 $.ui.toast(`New sessions start in ${modeLabel(mode)} mode${mode === 'bypassPermissions' ? `. ${BYPASS_WARNING}` : ''}`)
62}
63
64// Published for the optional menu; a failure here never blocks the pick itself.
65async function writeEntry($: EngineInterface) {
66 const entry: MenuEntry = { apiVersion: 1, title: 'Permissions', summary: `new sessions: ${modeLabel(await read($, defaultModeAtom))}`, order: 4 }
67 await $.state.set(menuEntryRef, entry).catch(() => undefined)
68}
69
70// The user's file value, the one the writes go to: always current. Pure (a render hook may call it).
71async function fileDefault($: EngineInterface): Promise<string> {
72 const path = await settingsPath($)
73 const settings = (await $.fs.exists(path)) ? parseSettings((await $.fs.read(path)) as string) : {}
74 return currentDefault(settings) ?? 'default'
75}
76
77// Into the atom (the menu entry reads it); when the file cannot be read the atom keeps its value.
78const loadDefault = ($: EngineInterface) => fileDefault($).then(value => update($, defaultModeAtom, () => value)).catch(() => undefined)
79
80async function page($: EngineInterface, e: any, hasBack: boolean) {
81 // the file may have changed since session.start (an edit, another session): re-read on every draw
82 const defaultMode = await fileDefault($).catch(() => read($, defaultModeAtom))
83 return permissionsPage($.ui.resolve(e), {
84 hasBack,
85 defaultMode,
86 onMode: mode => setDefaultMode($, mode).catch(error => $.ui.toast(`Could not set the default mode: ${why(error)}`)),
87 // the menu mod handles this press itself (ui.press on this plugin's key); the closure keeps the Button pressable
88 onBack: () => undefined,
89 })
90}
91
92export const register: Register = on => {
93 on('session.start', async ($, e, next) => {
94 // Nothing is written until the user picks: only the current value is read.
95 await loadDefault($)
96 await writeEntry($)
97 await $.command
98 .register({ name: 'permmode', description: 'Show the permission mode and pick the one new sessions start in', argumentHint: `[${MODES.join('|')}]` })
99 .catch(() => undefined)
100 return next(e)
101 })
102
103 on('command.run', { command: 'permmode' }, async ($, e) => {
104 const arg = e.args.trim()
105 if (arg !== '') {
106 if (!isMode(arg)) return { text: `Usage: /permmode [${MODES.join('|')}]` }
107 // The command may be invoked by the model: the mode that skips every check is a human pick, in the pane.
108 if (arg === 'bypassPermissions') return { text: 'bypassPermissions can only be picked in the pane; open it with /permmode.' }
109 try {
110 await setDefaultMode($, arg)
111 return { text: `New sessions start in ${modeLabel(arg)} mode.` }
112 } catch (error) {
113 return { text: `The default mode could not be changed: ${why(error)}` }
114 }
115 }
116 await $.ui.open({ id: PANE, title: 'Permissions', focus: true, closeOnEscape: true, rows: 17, columns: 56 })
117 return { text: 'Permission settings are open.' }
118 })
119
120 on('ui.render', { component: 'Pane', requestId: PANE }, ($, e) => page($, e, false))
121
122 // Inside the menu's pane (apiVersion 1): drawn only when the menu is on this panel.
123 on('ui.render', { component: 'Pane', requestId: MENU_PANE }, async ($, e, next) => {
124 const { value } = await $.state.get(menuPanel)
125 if (value !== 'permissions') return next(e)
126 return page($, e, true)
127 })
128}
129hooks/modes.ts 18 lines1export const MODES = ['default', 'plan', 'acceptEdits', 'auto', 'dontAsk', 'bypassPermissions'] as const
2
3export type Mode = (typeof MODES)[number]
4
5export const isMode = (v: unknown): v is Mode => MODES.includes(v as Mode)
6
7export const MODE_LABELS: Record<Mode, string> = {
8 default: 'ask',
9 plan: 'plan',
10 acceptEdits: 'accept edits',
11 auto: 'auto',
12 dontAsk: "don't ask",
13 bypassPermissions: 'bypass permissions',
14}
15
16// A value in settings.json that is not one of the modes stays as written.
17export const modeLabel = (m: string): string => (isMode(m) ? MODE_LABELS[m] : m)
18hooks/permissions-page.tsx 55 lines1import { MODES, modeLabel, type Mode } from './modes'
2
3export const BYPASS_WARNING = 'Bypass skips every permission check in every new session.'
4
5type Ui = { Box: any; Text: any; Button: any }
6
7// The Permissions page: the running mode (shown only) and the mode new sessions start in, one item per row.
8export function permissionsPage(
9 { Box, Text, Button }: Ui,
10 p: {
11 hasBack: boolean
12 defaultMode: string
13 onMode: (mode: Mode) => unknown
14 onBack: () => unknown
15 },
16) {
17 const heading = (text: string) => (
18 <Box key={`h-${text}`} marginTop={1}>
19 <Text bold>{text}</Text>
20 </Box>
21 )
22 return (
23 <Box flexDirection="column" paddingX={1}>
24 <Box justifyContent="space-between">
25 <Text bold color="claude">
26 ◆ Permissions
27 </Text>
28 {p.hasBack && <Button key="mods-back" plain dimColor hotkey="b" label="← back" onPress={p.onBack} />}
29 </Box>
30 <Text dimColor>The permission mode new sessions start in.</Text>
31 {heading('Running in')}
32 {/* The engine gives mods no permission-mode event or getter, so the running mode is unknown. */}
33 <Text key="mode-running">
34 unknown
35 <Text dimColor> · shift+tab switches it</Text>
36 </Text>
37 {heading('New sessions start in')}
38 {MODES.map(m => (
39 <Box key={`row-default-mode-${m}`} gap={1}>
40 <Button key={`default-mode-${m}`} plain label={`${m === p.defaultMode ? '●' : '○'} ${modeLabel(m)}`} dimColor={m !== p.defaultMode} onPress={() => p.onMode(m)} />
41 </Box>
42 ))}
43 {p.defaultMode === 'bypassPermissions' && (
44 <Text key="mode-warning" color="warning">
45 {BYPASS_WARNING}
46 </Text>
47 )}
48 <Text dimColor>Project or local settings can override this for a given folder.</Text>
49 <Box marginTop={1}>
50 <Text dimColor>↑↓ move · enter select · esc close</Text>
51 </Box>
52 </Box>
53 )
54}
55hooks/settings.ts 39 lines1// ~/.claude/settings.json edits on a parsed object: other keys are kept; an invalid file throws before anything is written.
2export type Settings = Record<string, any>
3
4// An error whose message is fixed text of this mod: safe to show. Anything else is shown as a generic line (see `why`).
5export class SettingsError extends Error {}
6
7export const why = (error: unknown): string =>
8 error instanceof SettingsError ? error.message : 'the settings file could not be read or written; nothing was changed.'
9
10export function parseSettings(text: string, name = 'settings.json'): Settings {
11 let s: unknown
12 try {
13 s = JSON.parse(text)
14 } catch {
15 throw new SettingsError(`${name} is not valid JSON; nothing was written.`)
16 }
17 if (!s || typeof s !== 'object' || Array.isArray(s)) throw new SettingsError(`${name} is not a JSON object; nothing was written.`)
18 return s as Settings
19}
20
21const ABSOLUTE = /^(\/|[A-Za-z]:[\\/]|\\\\)/
22
23// CLAUDE_CONFIG_DIR when it is an absolute path, else <home>/.claude; never a relative path.
24export function configDir(configDirEnv: string | undefined, home: string | undefined): string {
25 const dir = configDirEnv && ABSOLUTE.test(configDirEnv) ? configDirEnv : home ? `${home}/.claude` : ''
26 if (!ABSOLUTE.test(dir)) throw new SettingsError('no absolute CLAUDE_CONFIG_DIR, HOME or USERPROFILE: settings.json not touched')
27 return dir.replaceAll('\\', '/').replace(/\/+$/, '')
28}
29
30export const currentDefault = (s: Settings): string | undefined =>
31 typeof s.permissions?.defaultMode === 'string' ? s.permissions.defaultMode : undefined
32
33// Only permissions.defaultMode changes; a non-object `permissions` is refused rather than overwritten.
34export function withDefaultMode(s: Settings, mode: string): Settings {
35 const p = s.permissions
36 if (p !== undefined && (p === null || typeof p !== 'object' || Array.isArray(p))) throw new SettingsError('settings.json "permissions" is not an object; nothing was written.')
37 return { ...s, permissions: { ...p, defaultMode: mode } }
38}
39types/index.d.ts 13 lines1// Menu contract, apiVersion 1: an optional `menu` mod lists this entry.
2export type MenuEntry = { apiVersion: 1; title: string; summary: string; order: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 permissions: {
7 menuEntry: MenuEntry
8 // permissions.defaultMode of the user's settings.json: the mode new sessions start in
9 defaultMode: string
10 }
11 }
12}
13