Repository navigation
CapCut 7.x edits are discarded when the active draft lives under Timelines/ #50
Description
Activity
Hi @jaecho1001, thanks for the report and for the PR that goes with it.
The layout you describe is genuinely a new axis for this repo. #35/#39 covered the CapCut 8.7+ root mirror and
template-2.tmp, and the store currently knows three shapes:draft_content.jsonprimary,draft_info.jsonprimary (newer Mac builds), and mirror-only. A nestedTimelines/<main_timeline_id>/draft_info.jsonacting as the live document is not modeled anywhere, so if it holds, it is a real gap.Before I change which file the CLI treats as canonical, I need to be able to tell an active timeline apart from an archived one. Today a pre-8.7 project is served correctly from the root file, and that is the majority of users, so a wrong read here turns a silent no-op into propagated stale data.
Could you attach the following from the project you reproduced on:
Timelines/project.json, with ids left intact and any file paths or titles redacted as you see fit.- The file tree:
find "<project>" -name 'draft_*.json' -o -name 'template-2.tmp' -o -name 'project.json' - The exact version string CapCut wrote, from
draft_meta_info.json(ordraft_content.jsonif that is where it lands for you), plus the OS. - The before/after that shows the discard: content of
Timelines/<active-id>/draft_info.jsonimmediately after acapcut set-textrun that reported success, showing it still carries the old text while the root file carries the new one.
Point 4 is the one that decides it. If the nested file is stale after a successful write and CapCut then opens the old caption, the pointer is authoritative and your approach is right. If the nested file is simply an older snapshot that CapCut does not read, following the pointer would be the regression rather than the fix.
Is the project a CapCut 7.7 install you can still reproduce on, or a draft you received from someone else? That changes how much the version number tells us.
Rene
- added a commit that references this issue
on Aug 21, 2026 Same layout, different app generation: this reproduces on CapCut International for Mac 9.2.8, and there the nested file is authoritative in the way this issue describes. Posting it here rather than opening a new issue since it is the same axis. Note that this differs from the 8.5.0 observation in #60 / #68, where the nested layout did not discard tool writes.
Environment
- capcut-cli 0.20.0, Node 24.15.0
- macOS 26.3.1, Apple Silicon
- CapCut International for Mac,
Info.plistCFBundleShortVersionString9.2.0 - draft
last_modified_platform.app_version9.2.8-beta4,new_versionfield 183.0.0, schema int 360000 - app-created drafts contain no
draft_content.json;draft_info.jsonis the only primary
Reproduction
- Create an empty project in the app, then quit the app.
capcut quickstart test --template <empty-project> --video x.mp4 --srt x.srt --force-write- Lint is clean, the app lists the project, and the project opens without error.
- The timeline is empty — no video track, no text track.
What the app actually reads
App-created drafts carry
Timelines/project.jsonwith amain_timeline_id. The document CapCut 9.2.8 loads isTimelines/<main_timeline_id>/draft_info.json, mirrored inTimelines/<main_timeline_id>/template-2.tmpin the same directory. The project-rootdraft_info.jsonandtemplate-2.tmpbehave as legacy mirrors: after a successful CLI write they carry the new content while the nested file still carries the old (empty) timeline, and the app opens the nested one. That is the before/after in point 4 of the maintainer's list, at 9.2.8 rather than 7.7.The CLI writes only the two root-level files, so every write is a silent no-op from the app's point of view.
Workaround, verified by opening in the app
Copy the CLI-written root
draft_info.jsonover bothTimelines/<main_timeline_id>/draft_info.jsonandTimelines/<main_timeline_id>/template-2.tmp, setting the JSONidto the timeline id. After that the video track and the text track both appear as written.Unrelated smaller finding
editorProcesses()reported "CapCut is running" with no CapCut process present —ps -axo comm= | grep -i capcutwas empty, and calling the function directly from node returned[]. quickstart therefore refused to write without--force-write. Not reproduced deterministically, so mentioning it only in case it is a known race; happy to split it out if it is news.Fixture
Happy to provide a
capcut fixturebundle from this layout if it is useful for the 9.x case, but only once the bundle is confirmed to scrub device identifiers (#59).Thanks @k12ktv. One part of this I could confirm from the code without any artifact, and it's now fixed; the rest I can't move on yet.
The gap you point at is real, though not quite where the issue text puts it. The write path's silence on >= 8.7 is deliberate — CapCut creates
Timelines/<id>/draft_info.jsonroutinely on modern builds (#60 caught it doing so on an 8.5.0 open), so warning on every write would fire for the majority about a hazard nobody has evidenced. That stays.What was actually wrong is narrower and worse.
diagnosealready attaches the full redactednested_evidenceblock on a >= 8.7 store, andfixturealready bundlesTimelines/project.jsonand the nested documents — both keyed offnestedTimelines.length. But thediagnosenext_action and theversionsupport note were keyed off the layout value, which stops at 8.7 by design. So a 9.x user got a report carrying nested-Timelines evidence with no line of prose anywhere explaining what the structure was or that a bundle of it is the thing this issue has been waiting on. That's #95, fixed in #96 with a claim-free note — it asserts neither your discard finding nor #68's survival finding, because both predate the 8.7 storage change and neither transfers across it.What I still can't do is flip which file is canonical. That's PR #51's change, rejected once already pending a field artifact, and it repoints reads and writes for every store matching the layout.
Your stated blocker is gone. #59 was fixed in #66 and merged on 2026-08-09 —
device_id,mac_addressandhard_disk_idare redacted, andSANITIZE_REPORT.jsonno longer writes raw paths. That shipped in 0.18.0, so the 0.20.0 you're running already has it.capcut fixture <project> --out <dir>on the 9.2.8 project is unblocked today, and with #96 the tool will now tell you so.Two things I'd want pinned down before treating 9.2.8 as a confirmed second sighting:
-
editorProcesses()— I can't reproduce the shape you describe.src/store.tsis a statelessspawnSync("ps", ["-axo", "comm="])plus a substring filter, and its only caller insrc/draft.tsis gated onrunning.length > 0. Ifpswas empty and a direct call returned[], that message has no path to fire. Could you paste the exact stderr line? If it was the version-boundary refusal rather than the process check, that's a different bug and worth its own issue. -
The version string. You report
CFBundleShortVersionString9.2.0,app_version9.2.8-beta4, and title the report 9.2.8. Which doescapcut version <project>print for the store, and is this a public build or a beta channel? It changes how the registry ceiling should treat it.
Rene
-
Data point from CapCut 8.4.0-beta6 on macOS (capcut-cli 0.21.1) — this layout behaves differently from the 7.x behaviour described here, so it may help bound the fix.
What happened
- Created a draft with
capcut init <name> --template <copy-of-a-real-8.4-draft>. (The bundled 6.5.0 template produces a draft that 8.4 refuses to open — issue init: bundled template stamps version 7 / empty new_version, and CapCut 8.5.0 refuses to open the draft #67. Using a copy of an existing app-created draft as the template sidesteps it and yields a clean empty project with the correct schema.) add-video+add-texton the CLI-written root files. At this pointsync-timelinesreportedlayout: info-primary,nested_available: 0— noTimelines/directory existed, anddraft_info.json+template-2.tmpwere in sync.- Opened the project in CapCut 8.4.
Result: edits survived
CapCut opened the project correctly with both segments intact, and the app itself then created the nested layout —
Timelines/project.jsonplusTimelines/<guid>/with a full document set and.bakfiles.Re-reading after the app closed still shows the text segment:
ID Start -End Text db9ab327 0:00.20- 0:01.80 Prueba capcut-cliSo on 8.4/macOS the direction appears to be root → nested (the app materializes the nested documents from the root files on first open), rather than nested being the pre-existing source of truth that overwrites root. No
sync-timelines --nestedwas needed.Fixture
Redacted bundle (
capcut fixture, timeline JSON only, no media; 21 redactions — device IDs and macOS user): GIST_URL_HEREHappy to run any additional check on this machine if it helps narrow the version boundary.
- Created a draft with
Thanks @IrizaD — and the 8.4 band was genuinely thin, so this is welcome. Three things, one of which is a blocker on my side.
The fixture link didn't make it. The comment carries the literal placeholder
GIST_URL_HERE. A redacted bundle from an 8.4 store is the one artifact class this issue has been short of, so if you can paste the real URL I'll take it.Where I have to disagree on what the run shows. By your own step 2,
sync-timelinesreportednested_available: 0and noTimelines/directory existed when the CLI wrote. This issue is about a pre-existing nested document that the app reads while the CLI writes root — the stale-read case. In your run there was nothing nested to be stale: root was the only store, you wrote it, and the app then materialized the nested set from it. Survival was the only outcome available, so the run doesn't bear on the discard hypothesis either way. That's not a criticism of the report — the layout you describe is real and correctly observed. It's that the ordering makes it a different experiment.Your project is now the right test bed, though. It has an app-created nested layout sitting on disk. That is exactly the state the discard claim is about, and you can test it cheaply:
capcut set-text <project> <segment-id> "changed on 8.4"against the project as it stands now (post-open, nested present).- Without opening the app, diff root
draft_info.jsonagainstTimelines/<main_timeline_id>/draft_info.json— I expect root new, nested old. - Open in CapCut 8.4. Does the caption read the new text or the old one?
Step 3 is the whole question. New text → 8.4 regenerates nested from root and behaves like the 8.5.0 finding in #68. Old text → 8.4 is a discard build and this issue reaches lower than 7.x. Either answer is worth having.
What I'm not doing on this evidence. The CLI carries a version boundary at 8.5.0 (from #68) above which it tells users a root write will survive, because the 8.5.0 evidence was an open/close round trip on an existing nested document that came back byte-identical. Your report shows the app creating nested from root, which is a weaker claim than regenerating it over existing content. I'm not moving that boundary down to 8.4 until step 3 above says I can — telling an 8.4 user with a real nested layout that their edit is safe, on evidence that didn't cover that case, is the failure mode I'm trying to avoid.
On #67: the bundled template still declares 6.5.0 and the fix there was a warning plus the documented
--template <real draft>escape hatch, not a re-stamped template — so you hit expected behaviour and reached for the documented workaround independently. If the warning did not print when you raninit, tell me, because that part is supposed to be automatic.Rene
- added a commit that references this issue
on Aug 31, 2026 @k12ktv — three updates, and I owe you a correction on one of them.
Your
editorProcesses()finding was real, and I was wrong to doubt it. I said the message "has no path to fire" given an emptypsand a direct call returning[]. It had a path, and #99 found it independently:editorProcesses()substring-matched the entireps -axo comm=listing rather than comparing process names. Undernpx/npm exec, npm rewrites its own process title to the full command line, which containscapcut-cli— so the tool detected itself and refused with "CapCut is running". Your sample at rest was empty because the offending process only exists during the run, which is exactly why it didn't reproduce the way either of us checked it. That's fixed in #101, on master now. If you were on thenpxpath, that fully explains what you saw, and your instinct to report it was right.The tool no longer contradicts your report on 9.2.8. #102/#103 just merged:
versionTuplewas dropping pre-release markers, so9.2.8-beta4compared equal to a 9.2.8 release and inherited the "your root edit should survive the next open" reassurance derived from a release build in #68. A pre-release now gets its own wording that asserts neither survival nor discard, and names the gap. Your store was being told the opposite of what you observed; it isn't any more.Which also makes my earlier question actionable. I asked whether 9.2.8-beta4 was a public build or a beta channel, because it changes how the registry ceiling treats it. At the time the code couldn't have acted on your answer even if you'd given it — the distinction was flattened before it reached any gate. It can now, so the question is worth re-asking rather than being trivia.
What's still outstanding is the fixture. Your
#59blocker was cleared in #66 and shipped in 0.18.0 —device_id,mac_addressandhard_disk_idare redacted andSANITIZE_REPORT.jsonno longer writes raw paths — socapcut fixture <project> --out <dir>on the 9.2.8 project is safe to run today.Yours is still the strongest report on this issue: it describes the decisive before/after in prose, with a mechanism and a workaround you verified by opening the app. What's missing is the artifact, not the argument. A bundle from that project is what unblocks repointing which file the CLI treats as canonical, and I'm not making that change on prose alone — for anyone, including you.
If the 9.2.8 project is gone, say so and I'll stop asking; that's useful information too.
Rene
Thanks @k12ktv and @IrizaD — two things in v0.23.0 bear on the runs described here, without touching the canonical question.
init --template <dir>no longer copies the donor'sTimelines/(nor its.bakfiles or sidecar). Step 2 of the 9.2.8 reproduction —quickstart test --template <empty-project>— created the new draft with the donor's nested, empty timeline document already in place, andTimelines/project.jsonpointing at it: exactly the file this issue says the app reads. With 0.23.0 the new draft ships root documents only, so the same steps are a genuinely different experiment on 9.2.8: if the app materialisesTimelines/from the root documents the way 8.4.0 did in @IrizaD's run, the timeline shows up; if the draft still opens empty, that is the strongest discard evidence so far.Without
--template, the CLI now seeds new drafts from the store's newest app-authored project (markers and settings, none of the content), so theinit --template <copy-of-a-real-draft>workaround from the 8.4.0 report is the default — #67 / #111.The canonical flip stays gated on the artifact requested above: the nested
draft_info.jsonstill holding the old text right after aset-textthat reported success, on a store where the nested document pre-exists.capcut fixture <project> --out <dir>produces it redacted.Evidence from CapCut 8.7.0 (Windows): the app reads
Timelines/<id>/draft_content.json, and overwrites the root files with it on closeEnvironment: CapCut desktop 8.7.0.3685, Windows 11, capcut-cli 0.26.0. The project was created in the app (9:16 canvas, one 4K clip, one text segment), and then closed.
Method. With the app fully closed, we wrote a different sentinel text into the same text material of each timeline copy. Each write was a single atomic rename, and compact JSON with
ensure_ascii=Falseround-trips byte-identical:file sentinel draft_content.json(root)COPIA RADICETimelines/<id>/draft_content.jsonCOPIA TIMELINEStemplate-2.tmp(root)COPIA TEMPLATEdraft_content.json.bakleft untouched ( SOTTOTITOLO)Result 1: on open, the app shows
COPIA TIMELINES. The nested document is the one CapCut 8.7 loads. Opening alone changed onlydraft_settingsand created a.lockedfile in the project folder. It did not touch the timeline copies orroot_meta_info.json.Result 2: on close, the nested document wins everywhere. After closing the project:
- root
draft_content.jsonand roottemplate-2.tmpwere both rewritten with the nested content (COPIA TIMELINES); Timelines/<id>/template-2.tmpandTimelines/<id>/draft_content.json.bakwere created;.lockedwas removed androot_meta_info.jsonwas updated.
So on 8.7 a root-only CLI edit is not just ignored on open: it is discarded on the next close. This is the 8.7 counterpart of the 7.x behaviour described in this issue.
Practical consequence for capcut-cli users on 8.7: every write must be followed by
capcut sync-timelines <project> --nested --apply. In our pipeline we now treat that step as mandatory.Two side findings that may help:
.lockedis a per-project "open in editor" marker: it exists only while that project is open. It is finer-grained than detectingCapCut.exe(the process stays alive on the home screen).- Drafts built by
quickstart/compilehave noTimelines/folder. We have not yet checked what the app does with them on open and close.
Sanitized
capcut fixturebundle (files flattened,/→__): https://gist.github.com/fpisasale/8b344dccd4f4a731a35ef49ca55f0ff0 — the bundle of the project, taken after the close round-trip and one more root-onlyadd-text("SOLO RADICE"). It shows the root/nested divergence thatdiagnosereports. Note: the bundle needed one manual redaction, which we are reporting separately.Thanks for capcut-cli: it is the most solid tool we found for this.
— Spirito Digitale
- root
Thanks @fpisasale — this supplies the fixture and the experiment on a pre-existing nested timeline that I was waiting for. I checked the bundle: the root copies contain the additional
SOLO RADICEcaption while the selected nested document does not.I’m using it for regression coverage and an active-timeline fix, initially scoped to the evidenced Windows 8.7.0 build. The implementation will follow the selected timeline rather than copy the root into every timeline directory. Once the draft PR is ready, an open/close check with the patched CLI on your machine would confirm the app behavior.
I’m also addressing #133 and #134 in the same reviewable patch. The 7.x and Mac cases will remain separately unverified; this issue will stay open pending the patched Windows round-trip.
The patch is ready for review in draft PR #135: #135
Your field fixture is committed with attribution. On the evidenced Windows 8.7.0 layout, the selected active document drives reads and normal writes synchronize it with its readable mirrors and the root copies. Other timelines, IDs, and envelopes are preserved; assets and metadata remain at the project root.
sync-timelinesfollows active → root on this layout and keeps its newer-mirror gate.The full local suite passes (1,017 tests). This verifies file behavior, not the desktop round-trip. When you can test the PR, please edit a caption in a copy of your existing project, open it in CapCut 8.7.0.3685, close it, and confirm that the caption survives. An
add-videoand restore check would also help. The exact steps are indocs/reviews/issues-50-133-134.mdin the PR.This issue stays open pending that confirmation; the 7.x and Mac reports remain separate evidence gaps.
The patch is merged and published as v0.26.1; npm's
latestnow points to it.npm install -g capcut-cli@0.26.1 capcut --version
The packed release passed a fresh-install check against your contributed active-timeline fixture, including caption edits, document ID preservation, and restore. All 1,017 tests and the Windows/macOS/Linux CI checks passed.
The remaining check is the patched desktop round-trip on CapCut 8.7.0.3685: edit a caption in a backed-up copy of the existing project with CapCut closed, open and close it in the app, and confirm that the caption survives. The end-to-end guide includes the asset-import and restore checks. Review any old root-only edits shown as divergence before making a new edit, because the selected active timeline now drives writes on this layout.
Keeping this issue open for that confirmation and the separate 7.x/Mac evidence gaps.
Bug
A real CapCut 7.7 project stores its active document under
Timelines/<main_timeline_id>/draft_info.json. The project-rootdraft_info.jsonis a derived mirror that CapCut regenerates from the nested timeline when the project opens.The current draft-store discovery only considers root candidates. A write therefore reports success and changes the root mirror, but CapCut discards the edit on the next open.
Reproduction
With a CapCut 7.7 project whose text segment id is known:
Observed against a real project:
draft_info.jsoncontains the new text.Timelines/<active-id>/draft_info.jsonstill contains the old text.The relevant structural pointer is:
{ "id": "<timeline-id>", "main_timeline_id": "<timeline-id>" }in
Timelines/project.json.Expected
For verified pre-8.7 CapCut storage:
main_timeline_id(falling back toid) to the active nesteddraft_info.json, or nesteddraft_content.jsonwhen needed.sync-timelinesrepairs in the structurally safe nested-active → root-mirror direction.template-2.tmppriority remains unchanged.This is distinct from #35/#39, which cover the CapCut 8.7+ root mirror/template layout.