/ cli / handlers / autoMode.ts
autoMode.ts
  1  /**
  2   * Auto mode subcommand handlers — dump default/merged classifier rules and
  3   * critique user-written rules. Dynamically imported when `claude auto-mode ...` runs.
  4   */
  5  
  6  import { errorMessage } from '../../utils/errors.js'
  7  import {
  8    getMainLoopModel,
  9    parseUserSpecifiedModel,
 10  } from '../../utils/model/model.js'
 11  import {
 12    type AutoModeRules,
 13    buildDefaultExternalSystemPrompt,
 14    getDefaultExternalAutoModeRules,
 15  } from '../../utils/permissions/yoloClassifier.js'
 16  import { getAutoModeConfig } from '../../utils/settings/settings.js'
 17  import { sideQuery } from '../../utils/sideQuery.js'
 18  import { jsonStringify } from '../../utils/slowOperations.js'
 19  
 20  function writeRules(rules: AutoModeRules): void {
 21    process.stdout.write(jsonStringify(rules, null, 2) + '\n')
 22  }
 23  
 24  export function autoModeDefaultsHandler(): void {
 25    writeRules(getDefaultExternalAutoModeRules())
 26  }
 27  
 28  /**
 29   * Dump the effective auto mode config: user settings where provided, external
 30   * defaults otherwise. Per-section REPLACE semantics — matches how
 31   * buildYoloSystemPrompt resolves the external template (a non-empty user
 32   * section replaces that section's defaults entirely; an empty/absent section
 33   * falls through to defaults).
 34   */
 35  export function autoModeConfigHandler(): void {
 36    const config = getAutoModeConfig()
 37    const defaults = getDefaultExternalAutoModeRules()
 38    writeRules({
 39      allow: config?.allow?.length ? config.allow : defaults.allow,
 40      soft_deny: config?.soft_deny?.length
 41        ? config.soft_deny
 42        : defaults.soft_deny,
 43      environment: config?.environment?.length
 44        ? config.environment
 45        : defaults.environment,
 46    })
 47  }
 48  
 49  const CRITIQUE_SYSTEM_PROMPT =
 50    'You are an expert reviewer of auto mode classifier rules for Claude Code.\n' +
 51    '\n' +
 52    'Claude Code has an "auto mode" that uses an AI classifier to decide whether ' +
 53    'tool calls should be auto-approved or require user confirmation. Users can ' +
 54    'write custom rules in three categories:\n' +
 55    '\n' +
 56    '- **allow**: Actions the classifier should auto-approve\n' +
 57    '- **soft_deny**: Actions the classifier should block (require user confirmation)\n' +
 58    "- **environment**: Context about the user's setup that helps the classifier make decisions\n" +
 59    '\n' +
 60    "Your job is to critique the user's custom rules for clarity, completeness, " +
 61    'and potential issues. The classifier is an LLM that reads these rules as ' +
 62    'part of its system prompt.\n' +
 63    '\n' +
 64    'For each rule, evaluate:\n' +
 65    '1. **Clarity**: Is the rule unambiguous? Could the classifier misinterpret it?\n' +
 66    "2. **Completeness**: Are there gaps or edge cases the rule doesn't cover?\n" +
 67    '3. **Conflicts**: Do any of the rules conflict with each other?\n' +
 68    '4. **Actionability**: Is the rule specific enough for the classifier to act on?\n' +
 69    '\n' +
 70    'Be concise and constructive. Only comment on rules that could be improved. ' +
 71    'If all rules look good, say so.'
 72  
 73  export async function autoModeCritiqueHandler(options: {
 74    model?: string
 75  }): Promise<void> {
 76    const config = getAutoModeConfig()
 77    const hasCustomRules =
 78      (config?.allow?.length ?? 0) > 0 ||
 79      (config?.soft_deny?.length ?? 0) > 0 ||
 80      (config?.environment?.length ?? 0) > 0
 81  
 82    if (!hasCustomRules) {
 83      process.stdout.write(
 84        'No custom auto mode rules found.\n\n' +
 85          'Add rules to your settings file under autoMode.{allow, soft_deny, environment}.\n' +
 86          'Run `claude auto-mode defaults` to see the default rules for reference.\n',
 87      )
 88      return
 89    }
 90  
 91    const model = options.model
 92      ? parseUserSpecifiedModel(options.model)
 93      : getMainLoopModel()
 94  
 95    const defaults = getDefaultExternalAutoModeRules()
 96    const classifierPrompt = buildDefaultExternalSystemPrompt()
 97  
 98    const userRulesSummary =
 99      formatRulesForCritique('allow', config?.allow ?? [], defaults.allow) +
100      formatRulesForCritique(
101        'soft_deny',
102        config?.soft_deny ?? [],
103        defaults.soft_deny,
104      ) +
105      formatRulesForCritique(
106        'environment',
107        config?.environment ?? [],
108        defaults.environment,
109      )
110  
111    process.stdout.write('Analyzing your auto mode rules…\n\n')
112  
113    let response
114    try {
115      response = await sideQuery({
116        querySource: 'auto_mode_critique',
117        model,
118        system: CRITIQUE_SYSTEM_PROMPT,
119        skipSystemPromptPrefix: true,
120        max_tokens: 4096,
121        messages: [
122          {
123            role: 'user',
124            content:
125              'Here is the full classifier system prompt that the auto mode classifier receives:\n\n' +
126              '<classifier_system_prompt>\n' +
127              classifierPrompt +
128              '\n</classifier_system_prompt>\n\n' +
129              "Here are the user's custom rules that REPLACE the corresponding default sections:\n\n" +
130              userRulesSummary +
131              '\nPlease critique these custom rules.',
132          },
133        ],
134      })
135    } catch (error) {
136      process.stderr.write(
137        'Failed to analyze rules: ' + errorMessage(error) + '\n',
138      )
139      process.exitCode = 1
140      return
141    }
142  
143    const textBlock = response.content.find(block => block.type === 'text')
144    if (textBlock?.type === 'text') {
145      process.stdout.write(textBlock.text + '\n')
146    } else {
147      process.stdout.write('No critique was generated. Please try again.\n')
148    }
149  }
150  
151  function formatRulesForCritique(
152    section: string,
153    userRules: string[],
154    defaultRules: string[],
155  ): string {
156    if (userRules.length === 0) return ''
157    const customLines = userRules.map(r => '- ' + r).join('\n')
158    const defaultLines = defaultRules.map(r => '- ' + r).join('\n')
159    return (
160      '## ' +
161      section +
162      ' (custom rules replacing defaults)\n' +
163      'Custom:\n' +
164      customLines +
165      '\n\n' +
166      'Defaults being replaced:\n' +
167      defaultLines +
168      '\n\n'
169    )
170  }