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.
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 uncaughtOSErrorwhenever 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-enforcerMCP server'scapture_insighttool, which calls_run_keep(["put", insight] + tags)inkeep_enforcer.py:149. Any insight over ~255 bytes fails hard.Reproduce
Expected: note is stored.
Actual:
Root cause
cli_app.pyline 1085 in theputcommand:Path(source).is_dir()callsos.stat(source), which raisesOSError(ENAMETOOLONG)instead of returningFalsewheneversourceis 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 anyOSError(or at minimumENAMETOOLONG) falls through to inline-content handling:A tighter variant, fast-path obvious non-paths before touching the filesystem at all:
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, includingkeep-enforcer'scapture_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)
Workaround for agent clients
Pipe via stdin instead of passing as a positional argument:
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.