Repository navigation
Description
Version
1.0.4 (desktop app, bundle id com.kimi.code.desktop), engine 2.1.1 inside it. Source references are against main @ be7d5f5fea7800778e4660cd5f36780ba783bddd (apps/kimi-code@2.1.1), which is the commit the 1.0.4 bundle was built from.
Plan
Kimi Code subscription through the managed:kimi-code provider (OAuth, oauth_host = "https://auth.kimi.ai").
Model
DeepSeek/deepseek-flash (default_model in ~/.kimi-code/config.toml).
Platform
uname -mprs → Darwin 24.6.0 arm64 arm (Apple M4 Max, 36 GiB).
What I'm seeing
On the desktop app, the cwd field of the hook payload is "/" for every agent-level hook event, while the session it belongs to has a real working directory.
cwd is a documented top-level payload field and is meant to be the project directory — docs/en/customization/hooks.md:69-75:
{
"hook_event_name": "PreToolUse",
"session_id": "session_abc",
"session_title": "Fix the login page",
"client_type": "kimi_code_cli",
"cwd": "/path/to/project"
}Where the value comes from
- The field is assembled in
packages/agent-core-v2/src/features/externalHooks/internal/matchHooks.ts:44(const cwd = args.cwd ?? '';) and inserted into the payload at:60-63, then camel→snake converted at:124-134— which is why a plaingrep hook_event_nameover the source only finds the docs and tests, not the construction site. - The value is whatever the caller passes:
packages/agent-core-v2/src/features/externalHooks/app/externalHooksRunnerService.ts:92→cwd: args.cwd ?? this.bootstrap.cwd. bootstrap.cwdisinput.cwd ?? process.cwd()—packages/agent-core-v2/src/app/bootstrap/bootstrap.ts:123.- Nothing can supply
input.cwdon the desktop.ServerStartOptions(packages/kap-server/src/start.ts:99-127) has nocwdfield at all, and thebootstrap({...})call atstart.ts:198-212passeshomeDir,configPath,env,clientIdentityandargs— no cwd. - The agent-level hook service never passes one either. In
packages/agent-core-v2/src/features/externalHooks/agent/agentExternalHooksService.ts, thePreToolUsetrigger (:304),UserPromptSubmit(:343),Stop(:416),PreCompact(:428) and everything routed through itsfireAndForgethelper (:125-132) all omitcwd.
So the fallback is used, and it is process.cwd() of whatever process hosts the engine. For the desktop app that is the Electron main process, whose process.cwd() is / (LaunchServices starts it with / as the working directory). Result: cwd is the string "/".
Affected events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionResult, UserPromptSubmit, UserPromptQueued, Stop, StopFailure, Interrupt, PreCompact, PostCompact, Notification, TaskStarted, TurnStarted.
The session-scoped events are not affected — sessionExternalHooksService.ts:112/135/145/160/174 pass cwd: this.context.cwd, i.e. the session's workDir. Only the trigger sites that go through the runner's fallback are wrong.
This is not just a cosmetic payload field
matchHooks.ts:72 uses that same value as the hook process's working directory (cwd: hook.cwd ?? (cwd === '' ? undefined : cwd), consumed by runHook.ts:66-70 with shell: true, cwd). So a PreToolUse hook script runs in /, not in the project. Any hook that relies on a relative path, on os.getcwd(), or on the payload's cwd to locate the repository gets the wrong answer.
Measurements on this machine (desktop 1.0.4, no configuration changed)
lsof -a -p 1095 -d cwdfor the app's main process →cwdis/(its fds 0/1/2 are/dev/null). This is the value that becomesbootstrap.cwd.- A whole-machine sampler (
lsof -a -c python -d cwdin a tight loop, matching only hook command lines) caught 172 distinct hook subprocesses spawned by the engine in a 2 min 19 s window (2026-09-24 23:44:13Z–23:46:32Z): 172 / 172 ran withcwd = "/". The sessions that spawned them have real working directories (~/.kimi-code/session_index.jsonl→"workDir":"/Users/MacbookPro/Projects/<app-a>"and similar), and the app was the only Kimi Code engine running on the machine at the time. - A consumer already trips over it: the installed agent-acct hook derives a project label from the payload
cwd(basename of the path). Its cached client context hasproject_label: nullfor 5 / 5 desktop sessions on this machine, which is what acwdof/produces. It cannot fall back to a project name at all.
Steps to reproduce
-
Add a
PreToolUsehook that records both the payload field and its own working directory:[[hooks]] event = "PreToolUse" command = "/usr/bin/python3 -c 'import json,os,sys; d=json.load(sys.stdin); open(\"/tmp/hook-cwd.jsonl\",\"a\").write(json.dumps({\"payload_cwd\":d.get(\"cwd\"),\"process_cwd\":os.getcwd(),\"event\":d.get(\"hook_event_name\")})+\"\\n\")'" timeout = 10
-
In a desktop session whose working directory is not
/, run any tool call. -
cat /tmp/hook-cwd.jsonl→{"payload_cwd": "/", "process_cwd": "/", "event": "PreToolUse"}for every line, despite the session having a realworkDir. -
For contrast, a
SessionStarthook on the same session records the correct directory — the split between the two trigger paths is the bug.
Expected behavior
cwd should be the session's working directory (the workDir the session was created with), as documented, and hook scripts should be spawned in it.
Suggested fix (one of, or both):
- Pass the session cwd at the agent-level trigger sites in
agentExternalHooksService.ts(it already hasthis.sessionContextin scope, exactly likesessionExternalHooksServicedoes). - Give
ServerStartOptionsacwdand letbootstrapreceive it, so a host that is not the CLI (desktop, service, daemon) can hand over a real base directory instead of silently inheriting its ownprocess.cwd().
Notes
- Read-only observation:
lsof,ps, the local session index, and reading the shippedapp.asar. No process was killed and no configuration was changed to produce this report (step 1 above is the repro for anyone who wants to confirm it on their own machine). - Found while investigating a separate, larger issue — a session's resident child processes are never reaped (terminal tasks are skipped at dispose, and
reconcile()marks recordslostwithout signalling the persisted pid). I filed that one separately as [Bug] Resident child processes survive session end: terminal tasks are skipped at dispose and reconcile never signals the recorded pid #4028 so this field bug is not buried in it. - Scope note: the bug is the fallback itself, not the desktop. Any host that starts the engine with a
process.cwd()unrelated to the session (desktop, service, daemon) gets the same wrong value; only the desktop case is measured above.
Contribution
- I am willing to submit a PR for this bug fix myself (please wait for maintainer approval in this issue first).
Per CONTRIBUTING.md I will not open the PR before a maintainer comments /approve here.
Activity