CherryHQ/cherry-studio · error · Error

Invalid offset: ${offset + 1}. File has ${lines.length} line

Error message

Invalid offset: ${offset + 1}. File has ${lines.length} lines.

What it means

Thrown by the read tool when the 0-based offset (parsed.data.offset converted from 1-based, defaulting to 0) is negative or greater-than-or-equal to the number of lines. Line count comes from content.split('\n'), so an empty file yields length 1 (a single empty string). Any offset past the last line, or a negative offset from a bad input, triggers this before slicing.

Source

Thrown at src/main/ai/mcp/servers/filesystem/tools/read.ts:67

    }
    throw error
  }

  // Check if file is binary
  if (await isBinaryFile(validPath)) {
    throw new Error(`Cannot read binary file: ${filePath}`)
  }

  // Read file content
  const content = await fs.readFile(validPath, 'utf-8')
  const lines = content.split('\n')

  // Apply offset and limit
  const offset = (parsed.data.offset || 1) - 1 // Convert to 0-based
  const limit = parsed.data.limit || DEFAULT_READ_LIMIT

  if (offset < 0 || offset >= lines.length) {
    throw new Error(`Invalid offset: ${offset + 1}. File has ${lines.length} lines.`)
  }

  const selectedLines = lines.slice(offset, offset + limit)

  // Format output with line numbers and truncate long lines
  const output: string[] = []
  const relativePath = path.relative(baseDir, validPath)

  output.push(`File: ${relativePath}`)
  if (offset > 0 || limit < lines.length) {
    output.push(`Lines ${offset + 1} to ${Math.min(offset + limit, lines.length)} of ${lines.length}`)
  }
  output.push('')

  selectedLines.forEach((line, index) => {
    const lineNumber = offset + index + 1
    const truncatedLine = line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + '...' : line
    output.push(`${lineNumber.toString().padStart(6)}\t${truncatedLine}`)

View on GitHub (pinned to 726446b54c)

Solutions

  1. Cap offset at the file's line count before calling, or omit it to read from the start.
  2. Never pass offset=0; the schema is 1-based, so use offset >= 1 (or omit it for the default of 1).
  3. When paginating, derive the next offset from the previous read response and stop when it reports no more lines.

Example fix

// before
await handleReadTool({ file_path: 'small.txt', offset: 500 }, baseDir) // throws: Invalid offset

// after
await handleReadTool({ file_path: 'small.txt' }, baseDir) // omit offset, read from start
Defensive patterns

Strategy: validation

Validate before calling

const MAX = Number.MAX_SAFE_INTEGER
function clampOffset(requested: number | undefined, lineCount: number): number | undefined {
  if (requested === undefined) return undefined // default of 1 is always safe
  if (requested < 1) return 1
  if (requested > lineCount) return lineCount
  return requested
}

Type guard

function isValidOffset(offset: number | undefined, lineCount: number): boolean {
  const o = (offset ?? 1) - 1
  return o >= 0 && o < Math.max(lineCount, 1)
}

Try / catch

try {
  await handleReadTool(args, baseDir)
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid offset')) {
    // retry with offset omitted (read from start)
  } else throw e
}

Prevention

When it happens

Trigger: Calling read with offset larger than the file's line count (e.g. offset=500 on a 10-line file); offset=0 which becomes -1 after the 1-based conversion; reading a near-empty file with offset >= 2. Note offset defaults to 1 (0-based 0), so omitting offset is always safe.

Common situations: Caller computed offset from a stale line count (file shrunk since); offset passed as 0 by a caller assuming 0-based indexing; paginating past EOF without tracking the prior response's 'more lines' hint; empty file plus offset >= 2.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/a7bb1185d776602e. Report an issue: GitHub.