Skip to content

keep put "<long inline content>" crashes with OSError: [Errno 63] File name too long on macOS #8

@Daniels77777

Description

@Daniels77777

Hey Hugh, found this one tonight while Claude was helping me.

Keep version: v0.140.0
Platform: macOS (APFS/HFS+, 255-byte filename limit; Linux ext4 has the same limit and will reproduce as Errno 36 ENAMETOOLONG)
Python: 3.12

Summary

keep put "<inline string>" raises an uncaught OSError whenever the content string is longer than the filesystem's single-path-component limit (255 bytes on macOS/Linux). The content is never written. No user-actionable error message is surfaced, just a Python traceback.

Reliably reproduced 5 times via the keep-enforcer MCP server's capture_insight tool, which calls _run_keep(["put", insight] + tags) in keep_enforcer.py:149. Any insight over ~255 bytes fails hard.

Reproduce

# Works - short content
keep put "short note"

# Fails - content exceeds 255 bytes
keep put "$(python3 -c 'print("x" * 300)')"

Expected: note is stored.
Actual:

OSError: [Errno 63] File name too long: 'xxxxxxxx...'

Root cause

cli_app.py line 1085 in the put command:

else:
    # Detect file/URL/directory
    content = source
    uri = None
    watch_kind = "file"
    if source.startswith(("file://", "http://", "https://")):
        uri = source
        content = None
        watch_kind = "url" if source.startswith(("http://", "https://")) else "file"
    elif Path(source).is_dir():          # line 1085: crashes for long strings
        ...

Path(source).is_dir() calls os.stat(source), which raises OSError(ENAMETOOLONG) instead of returning False whenever source is longer than the single-path-component limit. The pathname-detection branch was meant to distinguish inline content from a path, but on long inline content the detection itself crashes before the inline fallback can take over.

Suggested fix

Guard the is_dir() check so any OSError (or at minimum ENAMETOOLONG) falls through to inline-content handling:

def _safe_is_dir(s: str) -> bool:
    try:
        return Path(s).is_dir()
    except OSError:
        # String too long, contains NULs, or otherwise not a valid path.
        # Treat as inline content.
        return False

...

elif _safe_is_dir(source):
    ...

A tighter variant, fast-path obvious non-paths before touching the filesystem at all:

def _looks_like_path(s: str) -> bool:
    # Inline note content rarely looks like a path. Filesystems cap
    # each component at 255 bytes; multi-line strings aren't paths;
    # NULs are illegal in POSIX paths.
    if len(s.encode("utf-8", errors="replace")) > 255 and "/" not in s:
        return False
    if "\n" in s or "\x00" in s:
        return False
    try:
        return Path(s).is_dir()
    except OSError:
        return False

Either variant ends the crash. The first is minimally invasive and matches the "fall through to inline" intent of the surrounding code.

Impact

Any client that pipes long strings through keep put "<content>" as a positional argument hits this, including keep-enforcer's capture_insight, which is a primary path for agents calling Keep from MCP. In practice that meant roughly half of attempted captures (ranging 300 to 1,400 bytes of intended note content) were silently lost until I noticed the tracebacks and shortened by hand. Stdin mode (keep put -) works fine because it bypasses this branch.

Traceback (one of five captured)

File "/Users/danielshurman/.local/share/uv/tools/keep-skill/lib/python3.12/site-packages/keep/cli_app.py", line 1085, in put
    elif Path(source).is_dir():
File ".../pathlib.py", line 876, in is_dir
    return S_ISDIR(self.stat().st_mode)
File ".../pathlib.py", line 841, in stat
    return os.stat(self, follow_symlinks=follow_symlinks)
OSError: [Errno 63] File name too long: '<~1400-byte insight string>'

Workaround for agent clients

Pipe via stdin instead of passing as a positional argument:

# In keep_enforcer.py, replace
# _run_keep(["put", insight] + tags)
# with:
subprocess.run([KEEP_PATH, "put", "-"] + tags, input=insight, text=True, check=True)

This avoids the broken branch entirely. Worth doing on the enforcer side regardless, since stdin is also safer for content containing quotes, backslashes, or shell metacharacters.

Happy to open a PR with the first-variant fix plus a regression test (test_put_long_inline_content) if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions